Skip to content

✨ 手写数组方法(map / reduce / filter)👌

要点速览

  • 行为一致:遵循规范操作(ToObjectlen >>> 0k in O)。
  • 稀疏数组:空位不调用回调、不写入结果;与 undefined 区分。
  • this 绑定:使用 callback.call(thisArg, ...) 与原生一致。
  • reduce 初始值:未传入时,从首个“真实存在”的元素开始;空数组报错。
  • 健壮性:调用者不可为 null/undefined,回调必须为函数;兼容类数组。

快速上手

最小可用的三段实现

js
// map:保序、跳过稀疏空位、支持 thisArg
Array.prototype.myMap = function (callback, thisArg) {
  if (this == null) throw new TypeError("this is null or not defined");
  if (typeof callback !== "function") {
    throw new TypeError("callback is not a function");
  }
  const O = Object(this);
  const len = O.length >>> 0;
  const result = new Array(len);
  for (let k = 0; k < len; k++) {
    if (k in O) result[k] = callback.call(thisArg, O[k], k, O);
  }
  return result;
};

// reduce:区分是否传入初始值;空数组无初始值需抛错
Array.prototype.myReduce = function (callback, initialValue) {
  if (this == null) throw new TypeError("this is null or not defined");
  if (typeof callback !== "function")
    throw new TypeError("callback is not a function");
  const O = Object(this);
  const len = O.length >>> 0;
  let k = 0,
    acc;
  if (arguments.length >= 2) {
    acc = initialValue;
  } else {
    while (k < len && !(k in O)) k++;
    if (k >= len)
      throw new TypeError("Reduce of empty array with no initial value");
    acc = O[k++];
  }
  for (; k < len; k++) if (k in O) acc = callback(acc, O[k], k, O);
  return acc;
};

// filter:结果数组长度不固定,仅保留回调返回真值的元素
Array.prototype.myFilter = function (callback, thisArg) {
  if (this == null) throw new TypeError("this is null or not defined");
  if (typeof callback !== "function")
    throw new TypeError("callback is not a function");
  const O = Object(this);
  const len = O.length >>> 0;
  const result = [];
  for (let k = 0; k < len; k++) {
    if (k in O && callback.call(thisArg, O[k], k, O)) result.push(O[k]);
  }
  return result;
};

边界与规范要点

  • Object(this):兼容类数组调用者(如 argumentsNodeList)。
  • len >>> 0:安全取非负整数长度,规避异常 length 值。
  • k in O:与稀疏数组行为一致,避免将空位误当 undefined
  • thisArg:第二参数作为回调 this 指向;否则为 undefined
  • reduce 初始值:不传初始值时,需寻找首个真实元素作为初始累加器。

手写 map 方法详解

常见误区

  • 直接 for 遍历,不判断 k in O,导致对稀疏空位错误调用。
  • 忽略 thisArg 绑定,使回调中的 this 与原生行为不一致。
  • 未做类型校验:thisnull/undefinedcallback 非函数时应抛错。

参考实现

javascript
/**
 * 模拟原生 Array.prototype.map 行为
 * @param {Function} callback - 映射回调函数 (value, index, array) => any
 * @param {*} [thisArg] - 指定回调内的 this 指向
 * @returns {Array} 新数组,长度与原数组一致,稀疏位置保持为空位
 */
Array.prototype.myMap = function (callback, thisArg) {
  // 1) 边界校验:调用者不可为 null/undefined;回调必须为函数
  if (this === null || this === undefined) {
    throw new TypeError("this is null or not defined");
  }
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }

  // 2) 将非对象值(原始类型值)转换为对应的包装对象,对于已为对象的值则直接返回其本身。
  const O = Object(this); // 相当于 new Object(this),确保调用者为对象

  // 3) 安全获取 length:无符号右移,确保为非负整数
  const len = O.length >>> 0;

  // 4) 预分配结果数组:与原数组等长(map 不修改原数组)
  const result = new Array(len);

  // 5) 主循环:仅在索引真实存在时调用回调
  for (let k = 0; k < len; k++) {
    // 使用 in 操作符跳过稀疏空位
    if (k in O) {
      const value = O[k]; // 当前索引的实际值
      // 绑定 thisArg;向回调传递 (value, index, array) 三个标准参数
      result[k] = callback.call(thisArg, value, k, O);
    }
  }

  // 6) 返回新数组:保持与原生 map 相同的稀疏结构
  return result;
};
规范说明:为何执行 Object(this)(ToObject)
  • 规范依据:ECMAScript 对多数内置方法都会执行 ToObject内部操作,将非对象值(原始类型值)转换为对应的包装对象,对于已为对象的值则直接返回其本身。。
  • 支持类数组:允许在非真正数组的“类数组对象”(如 argumentsNodeListHTMLCollection)上调用数组方法。
  • 原始值包装:当 this 是原始类型(如字符串、数字、布尔),会包装成对应的包装对象(如 new String(this)),从而可按索引访问其“属性”。
  • 稀疏语义一致:对数组与类数组,都用 k in O 判断“真实存在的索引”,避免把空位当作 undefined
  • 边界提醒:null/undefined 不能被 ToObject;因此在此之前需做空值校验并抛出 TypeError(与原生一致)。
  • 示例:
    • Array.prototype.myMap.call(arguments, x => x*2) 支持类数组参数列表。
    • Array.prototype.myMap.call("abc", ch => ch.toUpperCase())['A','B','C'](字符串被包装为 String 对象)。
规范说明:len >>> 0 的作用与原理
  • 作用:将 O.length 转为无符号 32 位整数(ToUint32),确保长度为非负整数,避免异常值(如小数、NaNInfinity、字符串)影响遍历。
  • 原理:位运算操作数在执行前会进行 ToUint32 转换;>>> 0 等价于“取该值的 32 位无符号整数表示”。
  • 为什么需要:数组方法实现需要使用“规范化的整数长度”,以与原生行为保持一致(近似 ToLength 的效果)。
  • 行为示例:
    • "5" >>> 05(字符串被转为数字)
    • 3.7 >>> 03(截断为整数)
    • NaN >>> 00
    • Infinity >>> 00
    • -1 >>> 04294967295(无符号 32 位的表示;真实数组不会出现负 length
  • 与规范的关系:ECMAScript 的 ToLength 上限为 2^53 − 1,而 >>> 0 上限为 2^32 − 1;数组的合法 length 最大为 2^32 − 1,因此在数组方法中两者表现一致。

map 行为要点

  • 空值校验:调用者不可为 null/undefinedcallback 必须为函数。
  • ToObject:使用 Object(this) 支持原始数据和类数组(如 argumentsNodeList)。
  • 长度规范化:len = O.length >>> 0 取无符号 32 位整数,保证非负。
  • 稀疏数组:用 k in O 跳过空位,保持与原生 map 一致的稀疏语义。
  • this 绑定:通过 callback.call(thisArg, ...) 保持与原生一致的 this 指向。
  • 复杂度:时间 O(n),空间 O(n)(返回新数组)。

行为一致性自测

js
// 1) 稀疏数组:原生 map 不处理空位
const a = [, 2, , 4];
console.log(
  a.map((x) => x),
  a.myMap((x) => x)
); // [empty, 2, empty, 4]

// 2) thisArg:保持与原生一致
const ctx = { mul: 3 };
console.log(
  [1, 2].myMap(function (x) {
    return x * this.mul;
  }, ctx)
); // [3,6]

// 3) 类数组:NodeList/arguments 等
function demo() {
  return Array.prototype.myMap.call(arguments, (x) => x * 2);
}
console.log(demo(1, 2, 3)); // [2,4,6]

手写 reduce 方法详解

核心难点

  • 初始值处理:有/无初始值两种分支;空数组且无初始值必须抛错
  • 稀疏数组跳过空位:寻找“首个真实存在的元素”作为初始值。

参考实现

javascript
/**
 * 模拟原生 Array.prototype.reduce 行为
 * @param {Function} callback - 规约回调 (accumulator, currentValue, index, array) => any
 * @param {*} [initialValue] - 累加器初始值;未传时以“首个真实存在元素”为初值
 */
Array.prototype.myReduce = function (callback, initialValue) {
  // 1) 边界校验:this 不可为 null/undefined
  if (this === null || this === undefined) {
    throw new TypeError("this is null or not defined");
  }
  // 2) 回调类型校验:callback 必须为函数
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }
  // 3) ToObject:兼容原始数据和类数组调用者
  const O = Object(this);
  // 4) 规范化长度:确保为非负整数
  const len = O.length >>> 0;
  let accumulator;
  let k = 0;
  // 5) 是否传入初始值:两种分支处理
  if (arguments.length >= 2) {
    accumulator = initialValue;
  } else {
    // 未传初始值:从数组中找到首个“真实存在”的元素
    while (k < len && !(k in O)) k++;
    // 空数组且无初始值:必须抛错(与原生一致)
    if (k >= len)
      throw new TypeError("Reduce of empty array with no initial value");
    accumulator = O[k++];
  }
  // 6) 主循环:跳过空位,依次规约更新累加器
  while (k < len) {
    if (k in O) {
      accumulator = callback.call(undefined, accumulator, O[k], k, O);
    }
  }
  return accumulator;
};

reduce 行为要点

  • 空值校验:调用者不可为 null/undefinedcallback 必须为函数。
  • ToObject:使用 Object(this) 支持原始数据类数组(如 argumentsNodeList)。
  • 长度规范化:len = O.length >>> 0 取无符号 32 位整数,保证非负。
  • 稀疏数组:用 k in O 跳过空位,与原生 reduce 行为一致。
  • 初始值分支:未传初始值时,寻找第一个“真实存在”的元素;空数组且无初始值必须抛错。
  • 复杂度:时间 O(n),空间 O(1)

行为一致性自测

js
console.log([1, 2, 3].myReduce((a, b) => a + b, 0)); // 6
console.log([, 2, , 4].myReduce((a, b) => a + b)); // 6(首个真实元素为 2)
try {
  [].myReduce((a, b) => a + b);
} catch (e) {
  console.log(e.message);
}

手写 filter 方法详解

参考实现

javascript
/**
 * 模拟原生 Array.prototype.filter 行为
 * @param {Function} callback - 筛选回调 (element, index, array) => boolean
 * @param {*} [thisArg] - 回调中的 this 指向
 */
Array.prototype.myFilter = function (callback, thisArg) {
  // 1) 边界校验:this 不可为 null/undefined
  if (this === null || this === undefined) {
    throw new TypeError("this is null or not defined");
  }
  // 2) 回调类型校验:callback 必须为函数
  if (typeof callback !== "function") {
    throw new TypeError(callback + " is not a function");
  }
  // 3) ToObject:兼容原始数据和类数组调用者
  const O = Object(this);
  // 4) 规范化长度:确保为非负整数
  const len = O.length >>> 0;
  const result = [];
  for (let k = 0; k < len; k++) {
    // 5) 跳过空位:仅对真实存在的索引调用回调
    if (k in O) {
      const v = O[k];
      // 6) 调用回调:仅当回调返回真值时,将当前元素 push 到结果数组
      if (callback.call(thisArg, v, k, O)) result.push(v);
    }
  }
  return result;
};

filter 行为要点

  • 空值校验:调用者不可为 null/undefinedcallback 必须为函数。
  • ToObject:使用 Object(this) 支持原始数据和类数组调用者。
  • 长度规范化:len = O.length >>> 0 保证非负整数长度。
  • 稀疏数组:用 k in O 跳过空位,不将空位视为 undefined
  • 真值保留:仅当回调返回真值时,将当前元素 push 到结果数组。
  • this 绑定:使用 callback.call(thisArg, ...) 绑定回调中的 this
  • 复杂度:时间 O(n),空间 O(n)(结果数组大小由命中次数决定)。

行为一致性自测

js
console.log([1, 2, 3, 4].myFilter((x) => x % 2 === 0)); // [2,4]
console.log([, 2, , 4].myFilter(() => true)); // [2,4](空位被跳过)

常见误区与解决方案

常见误区

  • 将稀疏数组空位当作 undefined 处理,导致与原生不一致。
  • 忽略 thisArg,使回调 this 指向错误。
  • 未做 callback 类型校验或 this 空值校验,规范性不足。
  • reduce 对空数组无初始值不抛错,违反原生行为。
误区场景问题原因解决方案
稀疏数组被错误遍历未使用 k in O 判断真实存在的索引in 操作符跳过空位
回调 this 指向不一致回调直接调用,未绑定 thisArg使用 callback.call(thisArg, ...)
reduce 初始值处理错不区分是否传入初始值、空数组未抛错按规范分支逻辑处理,空数组无初始值必须抛错

进阶优化:惰性求值与 Transducer

背景与动机

  • 链式调用的中间数组开销:arr.map().filter().reduce() 每步都会创建中间数组,导致额外的内存分配与 GC 压力,尤其是大数据量或高频调用的“热路径”。
  • 性能影响:中间数组不仅占用内存,还增加 CPU 拷贝与遍历次数,放大端到端延迟;在前端渲染(如列表、图表)与后端数据处理(日志、ETL)中尤为明显。
  • 流式/迭代数据:数据源常以“流/迭代器”形式提供(事件流、读取器、网络数据),不适合一次性构建多个中间集合;更需要“边遍历边处理”的管道式转换。
  • 惰性求值(迭代器/生成器):将转换延后到真正消费时执行,按需产出元素,天然消除中间数组,实现“只遍历一次”的流水线。
  • Transducer 模式:将多个转换(如 map + filter)合成为一个“高阶 reducer”,一次遍历即可完成所有转换,适配任意聚合器(pushconcatsum),无中间集合。
  • 典型适用场景:
    • 前端:长列表渲染、图形数据预处理、动画帧内数据管道(减少主线程压力)
    • 后端/Node:日志处理、CSV/JSON 流式解析、消息队列消费者(提高吞吐与降低内存峰值)

惰性管道(生成器示例)

js
function* map(iter, fn) {
  for (const x of iter) yield fn(x);
}
function* filter(iter, pred) {
  for (const x of iter) if (pred(x)) yield x;
}

const data = [1, 2, 3, 4, 5];
const pipeline = filter(
  map(data, (x) => x * 2),
  (x) => x % 3 === 0
);
console.log(Array.from(pipeline)); // [6]

Transducer 思路(仅遍历一次)

js
const mapping = (fn) => (reducer) => (acc, x) => reducer(acc, fn(x));
const filtering = (pred) => (reducer) => (acc, x) =>
  pred(x) ? reducer(acc, x) : acc;
const compose = (...fns) => fns.reduce((a, b) => (x) => a(b(x)));

const xf = compose(
  mapping((x) => x * 2),
  filtering((x) => x % 3 === 0)
);
const reducer = (acc, x) => {
  acc.push(x);
  return acc;
};
const result = [1, 2, 3, 4, 5].reduce(xf(reducer), []);
console.log(result); // [6]

面试答题核心要点

  1. 明确规范:ToObjectlen >>> 0k in OthisArg、reduce 初始值分支。
  2. 展示一致性:提供稀疏数组、this 绑定、类数组的对比自测示例。
  3. 体现工程思维:提及惰性求值与 Transducer,消除中间数组开销。

小结与后续

  • 手写方法的关键在“与原生行为一致”,先界定规范点再实现。
  • 自测用例覆盖稀疏数组、this 绑定、类数组、reduce 初始值四类场景。
  • 进一步可补充 forEachsomeevery 的实现与一致性测试,形成方法族。