人人都会AI编程

数组方法的手写实现与性能特点

更新时间:2026-07-11

理解数组方法的内部原理,不仅能让你在面试中应对手写代码题,更能帮助你在日常开发中做出正确的选型——知道某个方法背后做了什么,才能预判它对性能和内存的影响。本节挑选最常用的几个数组方法,逐一拆解实现思路,并分析它们的性能特点。

手写 forEach

forEach 对数组的每个元素执行一次给定的函数,没有返回值,纯粹用于副作用操作。

Array.prototype.myForEach = function(callback, thisArg) {
  // this 指向调用该方法的数组实例
  const arr = this;
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    // 跳过空位(稀疏数组)
    if (i in arr) {
      callback.call(thisArg, arr[i], i, arr);
    }
  }
};

关键点

  • this:方法内部的 this 就是调用该方法的数组对象。
  • thisArg:如果调用者传了第二个参数,回调函数内部通过 callthis 绑定到传入的对象,否则回调内的 this 就是 undefined(严格模式下)。
  • 稀疏数组处理:使用 i in arr 检查下标是否实际存在属性,跳过 undefined 的空位,这与原生行为一致。

性能特点

  • for 循环简单直接,时间复杂度 O(n)。
  • forEach 无法中途跳出(breakreturn 只能跳出本次回调,不会终止循环),如果确实需要中途退出,推荐用 for...of 或普通 for 循环。
  • 原生 forEach 的性能略低于 for 循环,因为每个元素都要调用一次函数,存在额外的函数调用开销。在热点路径上,如果性能敏感,优先使用 for 循环。

手写 map

map 对数组的每个元素调用回调,将返回值组成一个新数组返回,原数组保持不变。

Array.prototype.myMap = function(callback, thisArg) {
  const arr = this;
  const len = arr.length;
  const result = new Array(len); // 预分配空间
  for (let i = 0; i < len; i++) {
    if (i in arr) {
      result[i] = callback.call(thisArg, arr[i], i, arr);
    }
    // 空位保留(不填充任何值,result[i] 此时是 empty)
  }
  return result;
};

关键点

  • 预先创建好指定长度的数组 new Array(len),避免在循环中动态扩容。
  • 同样需要处理稀疏数组,保持与原数组一致的空位。
  • 返回值是一个新数组,原数组不受影响(纯函数特性)。

性能特点

  • 时间复杂度 O(n),空间复杂度 O(n)(需要存储新数组)。
  • map 适合“转换型”场景——将一组数据映射为另一组数据。如果不需要返回新数组(只做副作用),请用 forEach,避免不必要的内存分配。
  • for 循环相比,map 的声明式写法可读性更强,但在超大数组上可能有轻微的性能损失,通常不需要担心。

手写 filter

filter 对数组的每个元素调用回调,返回值为 true 的元素会被放入新数组。

Array.prototype.myFilter = function(callback, thisArg) {
  const arr = this;
  const len = arr.length;
  const result = [];
  for (let i = 0; i < len; i++) {
    if (i in arr) {
      const value = arr[i];
      if (callback.call(thisArg, value, i, arr)) {
        result.push(value);
      }
    }
  }
  return result;
};

性能特点

  • 时间复杂度 O(n),空间复杂度取决于匹配项数量。
  • 使用 push 往结果数组追加元素,而不是预分配数组,因为最终数组长度未知。大量 push 可能会触发数组扩容和内存复制,但现代引擎对 push 在连续内存上的处理已经高度优化。
  • 如果希望同时做筛选和转换,可以链式调用 filtermap,但会遍历两次。对于超大型数组,可以将逻辑合并到一个 reduce 中,一次遍历完成。

手写 reduce

reduce 将数组元素逐个处理,累积为一个值。它的灵活度最高,可以模拟 map、filter 等其他方法。

Array.prototype.myReduce = function(callback, initialValue) {
  const arr = this;
  const len = arr.length;
  let accumulator = initialValue;
  let startIndex = 0;

  // 如果没有提供初始值,取数组第一个有效值作为初始累加器
  if (arguments.length < 2) {
    // 寻找第一个非空位的元素
    while (startIndex < len && !(startIndex in arr)) {
      startIndex++;
    }
    if (startIndex >= len) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = arr[startIndex];
    startIndex++;
  }

  for (let i = startIndex; i < len; i++) {
    if (i in arr) {
      accumulator = callback(accumulator, arr[i], i, arr);
    }
  }
  return accumulator;
};

关键点

  • 对于 initialValue 未传的情况,需要找到第一个实际存在的元素作为初始累加器,然后从下一个位置开始遍历。这和原生行为完全一致。
  • 空数组且不传初始值会抛出 TypeError,这是 reduce 的唯一错误场景。

性能特点

  • 时间复杂度 O(n),每次迭代都更新累加器,典型的“折叠”操作。
  • reduce 因为灵活,代码可能不够直观,对于简单的求和、拍平操作很合适,但过于复杂的累加逻辑会降低可读性。团队中建议优先使用 mapfilter 等语义明确的方法,除非它们无法表达。

手写 find

find 返回第一个满足条件的元素,找不到返回 undefined

Array.prototype.myFind = function(callback, thisArg) {
  const arr = this;
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    if (i in arr && callback.call(thisArg, arr[i], i, arr)) {
      return arr[i]; // 找到立即返回
    }
  }
  return undefined;
};

性能特点

  • 一旦找到满足条件的元素,立即终止循环,不需要遍历剩余元素。
  • 这与 filter 不同,filter 必须遍历完毕。当只需要第一个匹配项时,find 明显更快。

手写 some / every

some 判断是否至少有一个元素满足条件,every 判断是否全部满足。

Array.prototype.mySome = function(callback, thisArg) {
  const arr = this;
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    if (i in arr && callback.call(thisArg, arr[i], i, arr)) {
      return true; // 短路
    }
  }
  return false;
};

Array.prototype.myEvery = function(callback, thisArg) {
  const arr = this;
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    if (i in arr && !callback.call(thisArg, arr[i], i, arr)) {
      return false; // 短路
    }
  }
  return true;
};

性能特点

  • 都支持短路运算:some 遇到 true 就返回,every 遇到 false 就返回,不会遍历多余元素。
  • 对于空数组,some 返回 falseevery 返回 true(数学上的空真),手写实现时要保持一致。

性能小结与选型建议

  • 普通 for 循环:性能之王,遍历开销最小,但代码相对繁琐且容易引入变量泄漏(var 声明时)。ES6 后 let 在块级作用域中更加安全。
  • forEach:语义清晰,适合只读遍历、执行副作用,但无法 break
  • map / filter / reduce:声明式风格,可组合,适合数据转换管道。额外函数调用和数组创建带来轻微开销,但在绝大多数业务场景都可忽略不计。
  • find / some / every:在查找或判断场景下使用,短路特性避免了不必要的遍历,性能优于全量遍历。

实用建议:先追求代码的正确性和可读性,再用性能分析工具定位真正的瓶颈。除非处理数万级以上的数组且处于 UI 切换的关键路径,否则这些方法的性能差异都比不上清晰的代码结构重要。