在 JavaScript 中,数组是最常用的引用类型之一。无论是渲染列表、管理状态、处理 API 返回的数据,还是实现队列和栈,数组都扮演着核心角色。ES6 之后,数组相关的 API 得到了极大的丰富,掌握其分类使用、性能特点和底层模拟实现,是每个开发者的必修课。
10.1.1 常用方法分类
数组的方法可以按照“是否修改原数组”和“主要用途”两个维度进行归类。在实际编码中,有意识地优先使用非破坏性方法,可以避免因意外副作用引发的 bug。
遍历方法
这些方法不会修改原数组,而是基于原数组生成新值或产生副作用。
forEach(callback)
纯粹遍历,没有返回值。不能用 break 或 return 中断循环,若需要提前退出,应使用 for...of 或 some/every。
[1, 2, 3].forEach(item => console.log(item));
map(callback)
返回一个新数组,每个元素是回调的返回值。常用于数据映射。
const doubled = [1, 2, 3].map(n => n * 2); // [2, 4, 6]
filter(callback)
返回新数组,包含所有通过测试的元素。适用于筛选项。
const evens = [1, 2, 3, 4].filter(n => n % 2 === 0); // [2, 4]
reduce(callback, initialValue)
将数组聚合成单个值。威力强大但可读性可能下降,建议配合有意义的初始值和变量名。
const sum = [1, 2, 3].reduce((acc, cur) => acc + cur, 0); // 6
some(callback)和every(callback)
返回布尔值。some 存在一个满足即 true;every 全部满足才为 true。两者在找到确定性结果时会提前终止。
修改方法
这些方法会直接修改原数组,使用时要特别留意是否有其他引用依赖。
push(...items)/pop()
尾部添加/删除一个元素,返回新长度或删除的元素。模拟栈的标准操作。
unshift(...items)/shift()
头部添加/删除一个元素,性能开销较大,因为需要移动所有后续元素的索引。
splice(start, deleteCount, ...items)
万能修改器:可从任意位置删除、插入或替换元素,直接修改原数组。返回被删除的元素组成的数组。
const arr = [1, 2, 3, 4];
arr.splice(1, 2, 'a', 'b'); // arr 变为 [1, 'a', 'b', 4]
reverse()/sort(compareFn)
反转或排序,均修改原数组。sort 默认将元素转为字符串比较,数字排序必须传入比较函数。
[3, 1, 10].sort((a, b) => a - b); // [1, 3, 10]
查询方法
indexOf(item, fromIndex)/lastIndexOf(item)
查找索引,找不到返回 -1。使用严格相等(===)。
includes(item, fromIndex)
判断是否包含某个值,返回布尔值。对 NaN 的判定比 indexOf 更正确。
find(callback)/findIndex(callback)
返回第一个满足条件的元素或其索引,找不到返回 undefined 或 -1。
转换与拼接
concat(...arrays)
合并一个或多个数组,返回新数组。常用作不可变操作。
slice(start, end)
提取子数组,返回新数组,不含 end 索引。是复制数组的轻量方法之一。
join(separator)
将数组元素连接成字符串,不影响原数组。
ES6+ 新增常用方法
Array.from(arrayLike, mapFn?)
将类数组或可迭代对象转换为真正的数组,可同时执行映射。
Array.of(...items)
创建新数组,统一行为(Array.of(3) 生成 [3],而非长度为 3 的空数组)。
flat(depth)/flatMap(callback)
扁平化数组,flat 默认拉平一层;flatMap 等于 map 后 flat(1)。
fill(value, start, end)
用固定值填充数组,会修改原数组。
10.1.2 类数组对象与数组转换
常见的类数组对象有:
- 函数内的
arguments对象(ES6 后推荐使用剩余参数替代) - DOM 方法返回的
NodeList(如document.querySelectorAll的结果) - 具有数字索引和
length属性的普通对象
它们有 length 和索引,但缺少数组方法。将其转换为真正数组的常用手段:
// 1. Array.from (最推荐)
const arr1 = Array.from(arguments);
const arr2 = Array.from(nodeList, el => el.textContent);
// 2. 扩展运算符 (仅限可迭代对象,NodeList 可迭代,arguments 也可)
const arr3 = [...nodeList];
// 3. Array.prototype.slice (兼容老旧环境)
const arr4 = Array.prototype.slice.call(arguments);
转换后,便可使用 map、filter 等全套数组方法,极大提升操作灵活性。
10.1.3 手写实现与性能特点
理解常用方法的手写实现,既能加深对方法行为的掌握,也常在面试中遇到。以下实现均为“不修改原数组”的版本,与原生行为保持一致。
手写 map
Array.prototype.myMap = function(callback, thisArg) {
if (typeof callback !== 'function') throw new TypeError('...');
const result = new Array(this.length);
for (let i = 0; i < this.length; i++) {
if (i in this) { // 跳过稀疏数组的空位
result[i] = callback.call(thisArg, this[i], i, this);
}
}
return result;
};
手写 filter
Array.prototype.myFilter = function(callback, thisArg) {
if (typeof callback !== 'function') throw new TypeError('...');
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this && callback.call(thisArg, this[i], i, this)) {
result.push(this[i]);
}
}
return result;
};
手写 reduce
Array.prototype.myReduce = function(callback, initialValue) {
if (typeof callback !== 'function') throw new TypeError('...');
let accumulator;
let startIndex = 0;
if (arguments.length >= 2) {
accumulator = initialValue;
} else {
// 无初始值时用第一个有效元素
while (startIndex < this.length && !(startIndex in this)) startIndex++;
if (startIndex >= this.length) throw new TypeError('Reduce of empty array with no initial value');
accumulator = this[startIndex++];
}
for (let i = startIndex; i < this.length; i++) {
if (i in this) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};
性能注意点
- 遍历选择:
for循环通常快于forEach,但现代引擎差距已极小。for...of简洁可用。forEach无法中断,需提前退出时用some/every或传统循环。 - 大数组的头部位移:
shift和unshift会触发 O(n) 的索引更新,频繁操作时可用双端队列思想,或使用splice(0, 1)也需谨慎;若只关心顺序,可倒过来用push/pop来模拟。 splice的小心:它在数组中删除/插入会造成后续元素移动,开销大。若仅需删除,可考虑直接赋length截断,或用filter/slice。sort性能:引擎内部通常使用优化过的排序算法(如 Timsort),但提供排序比较函数时要确保函数本身轻量,避免在排序中做重量级计算。- 稀疏数组处理:部分方法(如
map、filter)会跳过数组中的空位(hole),书写 polyfill 时需检查i in this以符合规范。
10.1.4 小结
数组对象是 JavaScript 中最频繁使用的复合数据类型之一。建议在日常开发中:
- 优先使用非破坏性方法(如
map、filter、concat、slice),减少副作用。 - 根据场景选择方法:查找用
find/includes,聚合用reduce,条件判断用some/every。 - 注意性能边界:大数组避免高频
shift/unshift/splice,谨慎使用sort比较函数。 - 务必理解类数组的转换,这在 DOM 操作和旧代码中会频繁遇到。
- 手写实现是理解原理的绝佳途径,掌握它们后,阅读和排查框架源码的能力会显著提升。