人人都会AI编程

10.1 数组对象

更新时间:2026-07-11

在 JavaScript 中,数组是最常用的引用类型之一。无论是渲染列表、管理状态、处理 API 返回的数据,还是实现队列和栈,数组都扮演着核心角色。ES6 之后,数组相关的 API 得到了极大的丰富,掌握其分类使用、性能特点和底层模拟实现,是每个开发者的必修课。

10.1.1 常用方法分类

数组的方法可以按照“是否修改原数组”和“主要用途”两个维度进行归类。在实际编码中,有意识地优先使用非破坏性方法,可以避免因意外副作用引发的 bug。

遍历方法

这些方法不会修改原数组,而是基于原数组生成新值或产生副作用。

  • forEach(callback)

纯粹遍历,没有返回值。不能用 breakreturn 中断循环,若需要提前退出,应使用 for...ofsome/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 存在一个满足即 trueevery 全部满足才为 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 等于 mapflat(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);

转换后,便可使用 mapfilter 等全套数组方法,极大提升操作灵活性。

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 或传统循环。
  • 大数组的头部位移shiftunshift 会触发 O(n) 的索引更新,频繁操作时可用双端队列思想,或使用 splice(0, 1) 也需谨慎;若只关心顺序,可倒过来用 push/pop 来模拟。
  • splice 的小心:它在数组中删除/插入会造成后续元素移动,开销大。若仅需删除,可考虑直接赋 length 截断,或用 filter/slice
  • sort 性能:引擎内部通常使用优化过的排序算法(如 Timsort),但提供排序比较函数时要确保函数本身轻量,避免在排序中做重量级计算。
  • 稀疏数组处理:部分方法(如 mapfilter)会跳过数组中的空位(hole),书写 polyfill 时需检查 i in this 以符合规范。

10.1.4 小结

数组对象是 JavaScript 中最频繁使用的复合数据类型之一。建议在日常开发中:

  1. 优先使用非破坏性方法(如 mapfilterconcatslice),减少副作用。
  2. 根据场景选择方法:查找用 find/includes,聚合用 reduce,条件判断用 some/every
  3. 注意性能边界:大数组避免高频 shift/unshift/splice,谨慎使用 sort 比较函数。
  4. 务必理解类数组的转换,这在 DOM 操作和旧代码中会频繁遇到。
  5. 手写实现是理解原理的绝佳途径,掌握它们后,阅读和排查框架源码的能力会显著提升。