{T}

手写数组方法既能加深对 JavaScript 语言本身的理解,也是面试中的高频考题。本文基于 ECMA 规范,剖析并手写 pushpopmapreducefilterslicefindeverysomeindexOfflatconcat 等常用方法的底层逻辑,并给出对应的实现。

push 方法的底层实现

push 的作用是在数组末尾追加一个或多个元素,并返回新的长度。ECMA 规范中的定义如下:

text
When the push method is called with zero or more arguments, the following steps are taken:
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. Let argCount be the number of elements in items.
4. If len + argCount > 2^53 - 1, throw a TypeError exception.
5. For each element E of items, do
  a. Perform ? Set(O, ! ToString(F(len)), E, true).
  b. Set len to len + 1.
6. Perform ? Set(O, "length", F(len), true).
7. Return F(len).

根据上述规范,转换为代码:

javascript
Array.prototype.push = function(...items) {
  let O = Object(this);              // 规范中的 ToObject:先转换为对象
  let len = this.length >>> 0;       // LengthOfArrayLike
  let argCount = items.length >>> 0;
  // 2^53 - 1 为 JS 能表示的最大安全正整数
  if (len + argCount > 2 ** 53 - 1) {
    throw new TypeError("The number of array is over the max value");
  }
  for (let i = 0; i < argCount; i++) {
    O[len + i] = items[i];           // 依次追加元素
  }
  let newLength = len + argCount;
  O.length = newLength;              // 更新 length
  return newLength;
};

核心思路:向数组自身循环追加新元素,并把 length 调整为最新长度。其中对长度做 >>> 0 无符号位移,是为了将值规范化为 0 到 2^32-1 之间的无符号整数,这在很多底层源码中都会出现。

pop 方法的底层实现

pop 的作用是删除并返回数组的最后一个元素,数组为空时返回 undefined。ECMA 规范:

text
When the pop method is called, the following steps are taken:
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If len = 0, then
    Perform ? Set(O, "length", +0F, true).
    Return undefined.
4. Else,
  Assert: len > 0.
  Let newLen be F(len - 1).
  Let index be ! ToString(newLen).
  Let element be ? Get(O, index).
  Perform ? DeletePropertyOrThrow(O, index).
  Perform ? Set(O, "length", newLen, true).
  Return element.

转换为代码:

javascript
Array.prototype.pop = function() {
  let O = Object(this);
  let len = this.length >>> 0;
  if (len === 0) {
    O.length = 0;
    return undefined;                // 空数组返回 undefined
  }
  len--;
  let value = O[len];
  delete O[len];                     // 删除最后一个元素
  O.length = len;                    // 更新长度
  return value;
};

核心思路:删掉数组最后一个元素,更新 length,并返回被删除的元素。需要注意空数组时返回 undefined 的特殊处理。

map 方法的底层实现

map 的作用是对数组每个元素执行回调,返回由回调返回值组成的新数组。ECMA 规范:

text
When the map method is called with one or two arguments, the following steps are taken:
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callbackfn) is false, throw a TypeError exception.
4. Let A be ? ArraySpeciesCreate(O, len).
5. Let k be 0.
6. Repeat, while k < len,
    a. Let Pk be ! ToString(F(k)).
    b. Let kPresent be ? HasProperty(O, Pk).
    c. If kPresent is true, then
        Let kValue be ? Get(O, Pk).
        Let mappedValue be ? Call(callbackfn, thisArg, « kValue, F(k), O »).
        Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
    d. Set k to k + 1.
7. Return A.

转换为代码:

javascript
Array.prototype.map = function(callbackfn, thisArg) {
  // 处理 this 为 null/undefined
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'map' of null");
  }
  // 处理回调非函数
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    throw new TypeError(callbackfn + " is not a function");
  }
  let O = Object(this);
  let T = thisArg;
  let len = O.length >>> 0;
  let A = new Array(len);            // 新数组
  for (let k = 0; k < len; k++) {
    if (k in O) {
      let kValue = O[k];
      // 依次传入 this、当前项、当前索引、整个数组
      let mappedValue = callbackfn.call(T, kValue, k, O);
      A[k] = mappedValue;
    }
  }
  return A;                          // 返回新数组,不改变原数组
};

map 的实现建立在 push/pop 的思路之上,关键在于遍历时调用回调并把返回值存入新数组,且不修改原数组。

reduce 方法的底层实现

reduce 的作用是对数组元素依次执行回调,将结果累积为单个值。ECMA 规范:

text
When the reduce method is called with one or two arguments, the following steps are taken:
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callbackfn) is false, throw a TypeError exception.
4. If len = 0 and initialValue is not present, throw a TypeError exception.
5. Let k be 0.
6. Let accumulator be undefined.
7. If initialValue is present, then
    Set accumulator to initialValue.
8. Else,
    Let kPresent be false.
    Repeat, while kPresent is false and k < len,
        Let Pk be ! ToString(F(k)).
        Set kPresent to ? HasProperty(O, Pk).
        If kPresent is true, then
        Set accumulator to ? Get(O, Pk).
        Set k to k + 1.
    If kPresent is false, throw a TypeError exception.
9. Repeat, while k < len, ...
10. Return accumulator.

转换为代码:

javascript
Array.prototype.reduce = function(callbackfn, initialValue) {
  // 处理 this 为 null/undefined
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'reduce' of null");
  }
  // 处理回调非函数
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    throw new TypeError(callbackfn + " is not a function");
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  let accumulator;

  // 未提供初始值时,取第一个存在的元素作为累加器
  if (arguments.length < 2) {
    let kPresent = false;
    while (!kPresent && k < len) {
      kPresent = k in O;
      if (kPresent) accumulator = O[k];
      k++;
    }
    // 数组为空且无初始值,抛出异常
    if (!kPresent) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
  } else {
    accumulator = initialValue;
  }

  for (; k < len; k++) {
    if (k in O) {
      // reduce 的核心:累加器不断累积
      accumulator = callbackfn.call(undefined, accumulator, O[k], O);
    }
  }
  return accumulator;
};

两个关键点需要注意:

  • 初始值的特殊处理:未传初始值时,需跳过空槽取第一个存在的元素作为累加器;若数组为空则抛 TypeError
  • 累加器逻辑:每次把上一次的结果 accumulator 作为回调的第一个参数传入。

slice 方法的底层实现

slice 返回由 startend(不含)之间的元素组成的新数组,不改变原数组。支持负数索引。

javascript
Array.prototype.slice = function(start, end) {
  let O = Object(this);
  let len = O.length >>> 0;
  // 处理 start:undefined 视为 0,负数则从末尾开始计算
  let k = start === undefined ? 0 : start;
  if (k < 0) k = Math.max(len + k, 0);
  else k = Math.min(k, len);
  // 处理 end:undefined 视为 len,负数则从末尾开始计算
  let final = end === undefined ? len : end;
  if (final < 0) final = Math.max(len + final, 0);
  else final = Math.min(final, len);

  let count = Math.max(final - k, 0);
  let A = new Array(count);
  let n = 0;
  while (k < final) {
    if (k in O) A[n] = O[k];
    k++;
    n++;
  }
  return A;
};

核心思路:先规范化 start/end(兼容负数索引),再遍历取值放入新数组。slice 不会修改原数组,因此常用于拷贝数组的浅拷贝场景。

filter 方法的底层实现

filter 返回由所有满足回调条件的元素组成的新数组。不满足条件的元素被过滤掉。

javascript
Array.prototype.filter = function(callbackfn, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'filter' of null");
  }
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    throw new TypeError(callbackfn + " is not a function");
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let A = [];
  let k = 0;
  let to = 0;
  while (k < len) {
    if (k in O) {
      let kValue = O[k];
      // 只有回调返回 true 的元素才放入新数组
      if (callbackfn.call(thisArg, kValue, k, O)) {
        A[to++] = kValue;
      }
    }
    k++;
  }
  return A;
};

核心思路:遍历数组,对满足条件的元素执行 push 到新数组,最终返回过滤后的新数组,原数组不变。

find 方法的底层实现

find 返回数组中第一个满足回调条件的元素值,没有则返回 undefined

javascript
Array.prototype.find = function(callbackfn, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'find' of null");
  }
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    throw new TypeError(callbackfn + " is not a function");
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  while (k < len) {
    let kValue = O[k];
    // 命中第一个满足条件的元素立即返回
    if (callbackfn.call(thisArg, kValue, k, O)) {
      return kValue;
    }
    k++;
  }
  return undefined;
};

核心思路:遍历数组,遇到第一个满足条件的元素立即返回,整体采用「短路」逻辑,因此比 filter 性能更高(不遍历完整数组)。

every 与 some 方法的底层实现

every 判断数组所有元素是否都满足条件;some 判断是否至少一个元素满足条件。两者都返回布尔值。

javascript
Array.prototype.every = function(callbackfn, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'every' of null");
  }
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    throw new TypeError(callbackfn + " is not a function");
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  while (k < len) {
    if (k in O && !callbackfn.call(thisArg, O[k], k, O)) {
      return false;                    // 出现一个不满足即返回 false
    }
    k++;
  }
  return true;
};

Array.prototype.some = function(callbackfn, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'some' of null");
  }
  if (Object.prototype.toString.call(callbackfn) !== "[object Function]") {
    throw new TypeError(callbackfn + " is not a function");
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  while (k < len) {
    if (k in O && callbackfn.call(thisArg, O[k], k, O)) {
      return true;                     // 出现一个满足即返回 true
    }
    k++;
  }
  return false;
};

everysome 都是短路求值:every 遇不满足即返回 falsesome 遇满足即返回 true,两者都能提前结束遍历。

indexOf 方法的底层实现

indexOf 返回指定元素在数组中首次出现的位置,不存在则返回 -1

javascript
Array.prototype.indexOf = function(searchElement, fromIndex) {
  let O = Object(this);
  let len = O.length >>> 0;
  if (len === 0) return -1;
  // 处理 fromIndex:默认从 0 开始,支持负数
  let n = fromIndex === undefined ? 0 : fromIndex;
  let k;
  if (n >= 0) {
    k = Math.min(n, len - 1);
  } else {
    k = len - Math.abs(n);             // 从末尾开始计数
    if (k < 0) k = 0;
  }
  while (k < len) {
    if (k in O && O[k] === searchElement) {
      return k;
    }
    k++;
  }
  return -1;
};

核心思路:从 fromIndex(支持负数)开始,使用全等运算符 === 逐个比较,找到即返回索引,否则返回 -1

flat 方法的底层实现

flat(depth) 将数组按指定深度递归展平,返回新数组,不修改原数组。默认深度为 1。

javascript
Array.prototype.flat = function(depth = 1) {
  let O = Object(this);
  let len = O.length >>> 0;
  let result = [];
  const flatDepth = (arr, d) => {
    for (let i = 0; i < arr.length; i++) {
      const item = arr[i];
      // 若仍是数组且未达到深度限制,则继续递归展平
      if (Array.isArray(item) && d > 0) {
        flatDepth(item, d - 1);
      } else {
        result.push(item);
      }
    }
  };
  flatDepth(O, depth);
  return result;
};

核心思路:借助递归 + 深度计数,遇到嵌套数组且深度未耗尽时继续递归,否则把元素推入结果。传入 Infinity 可展平任意深度。

concat 方法的底层实现

concat 将参数(数组或单个值)连接到原数组末尾,返回新数组,不修改原数组。若参数是数组则展开一层。

javascript
Array.prototype.concat = function(...items) {
  let O = Object(this);
  let A = [];
  // 先拷贝原数组元素
  for (let i = 0; i < O.length; i++) {
    A.push(O[i]);
  }
  // 处理每个参数:数组展开一层,其余直接追加
  for (let j = 0; j < items.length; j++) {
    const item = items[j];
    if (Array.isArray(item)) {
      for (let k = 0; k < item.length; k++) {
        A.push(item[k]);
      }
    } else {
      A.push(item);
    }
  }
  return A;
};

核心思路:先拷贝自身元素,再遍历参数——数组参数展开一层后追加,非数组值直接追加,最终返回拼接后的新数组。

V8 源码参考

V8 引擎中数组方法的实现源码(供对照参考):

方法V8 源码
poppop 的实现
pushpush 的实现
mapmap 的实现
sliceslice 的实现
filterfilter 的实现
findfind 的实现
everyevery 的实现
somesome 的实现
indexOfindexOf 的实现
concatconcat 的实现

总结

手写数组方法的关键在于理解每个方法的规范步骤,并把它们转化为代码。本文覆盖了 pushpopmapreduce 四个核心方法,并补充了 slicefilterfindeverysomeindexOfflatconcat 等常用方法的手写实现。

整体可归纳为几类思路:

  • 栈操作类push/pop):依赖数组自身的 length 与索引追加、删除。
  • 遍历回调类map/filter/find/every/some):通过 for 循环 + 回调,关键是处理好回调参数、thisArg 与返回值。
  • 切片拼接类slice/concat/flat):返回新数组,不修改原数组,重点处理索引规范化与嵌套展开。

建议在此基础上举一反三,自己实现 reduceRightflatMapsort 等更多方法,从而系统化地掌握数组机制的底层原理。