手写 JS 数组多个方法的底层实现

我们都知道,比较常用的数组方法有 push、pop、slice、map 和 reduce 等。

  • reduce 方法里面的参数都是什么作用?

  • push 和 pop 的底层逻辑是什么样的呢?

push 方法的底层实现

为了更好地实现 push 的底层方法,可以先去 ECMA 的官网去查一下关于 push 的基本描述(链接:ECMA 数组的 push 标准),我们看下其英文的描述,如下所示。

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

从上面的描述可以看到边界判断逻辑以及实现的思路,根据这段英文,我们将其转换为容易理解代码,如下所示。

js 复制代码
Array.prototype.push = function(...items) {
  let O = Object(this);  // ecma 中提到的先转换为对象
  let len = this.length >>> 0;
  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;
  return newLength;
}

从上面的代码可以看出,关键点就在于给数组本身循环添加新的元素 item,然后调整数组的长度 length 为最新的长度,即可完成 push 的底层实现。

其中关于长度的部分需要做无符号位移,无符号位移在很多源码中都会看到。

pop 方法的底层实现

同样我们也一起来看下 pop 的底层实现,也可以先去 ECMA 的官网去查一下关于 pop 的基本描述(链接:ECMA 数组的 pop 标准),我们还是同样看下英文的描述。

js 复制代码
When the pop method is called, the following steps are taken:
Let O be ? ToObject(this value).
Let len be ? LengthOfArrayLike(O).
If len = 0, then
 Perform ? Set(O, "length", +0F, true).
 Return undefined.
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.

从上面的描述可以看到边界判断逻辑以及实现的思路,根据上面的英文,我们同样将其转换为可以理解的代码,如下所示。

js 复制代码
Array.prototype.pop = function() {
  let O = Object(this);
  let len = this.length >>> 0;
  if (len === 0) {
    O.length = 0;
    return undefined;
  }
  len --;
  let value = O[len];
  delete O[len];
  O.length = len;
  return value;
}

其核心思路还是在于删掉数组自身的最后一个元素,index 就是数组的 len 长度,然后更新最新的长度,最后返回的元素的值,即可达到想要的效果。另外就是在当长度为 0 的时候,如果执行 pop 操作,返回的是 undefined,需要做一下特殊处理。

看完了 pop 的实现,我们再来看一下 map 方法的底层逻辑。

map 方法的底层实现

同样可以去 ECMA 的官网去查一下关于 map 的基本描述(链接:ECMA 数组的 map 标准),请看英文的表述。

js 复制代码
When the map method is called with one or two arguments, the following steps are taken:
Let O be ? ToObject(this value).
Let len be ? LengthOfArrayLike(O).
If IsCallable(callbackfn) is false, throw a TypeError exception.
Let A be ? ArraySpeciesCreate(O, len).
Let k be 0.
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.
Return A.

同样的,根据上面的英文,我们将其转换为可理解的代码,如下所示。

js 复制代码
Array.prototype.map = function(callbackFn, thisArg) {
  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;
}

有了上面实现 push 和 pop 的基础思路,map 的实现也不会太难了,基本就是再多加一些判断,循环遍历实现 map 的思路,将处理过后的 mappedValue 赋给一个新定义的数组 A,最后返回这个新数组 A,并不改变原数组的值。

最后我们来看看 reduce 的实现。

reduce 方法的底层实现

ECMA 官网关于 reduce 的基本描述(链接:ECMA 数组的 pop 标准),如下所示。

js 复制代码
When the reduce method is called with one or two arguments, the following steps are taken:
Let O be ? ToObject(this value).
Let len be ? LengthOfArrayLike(O).
If IsCallable(callbackfn) is false, throw a TypeError exception.
If len = 0 and initialValue is not present, throw a TypeError exception.
Let k be 0.
Let accumulator be undefined.
If initialValue is present, then
 Set accumulator to initialValue.
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.
Repeat, while k < len,
 Let Pk be ! ToString(F(k)).
 Let kPresent be ? HasProperty(O, Pk).
 If kPresent is true, then
     Let kValue be ? Get(O, Pk).
     Set accumulator to ? Call(callbackfn, undefined, << accumulator, kValue, F(k), O >>).
 Set k to k + 1.
Return accumulator.

还是将其转换为我们自己的代码,如下所示。

js 复制代码
Array.prototype.reduce  = function(callbackfn, initialValue) {
  // 异常处理,和 map 类似
  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 = initialValue;  // reduce方法第二个参数作为累加器的初始值
  if (accumulator === undefined) {      throw new Error('Each element of the array is empty');
      // 初始值不传的处理
    for(; k < len ; k++) {
      if (k in O) {
        accumulator = O[k];
        k++;
        break;
      }
    }
  }
  for(;k < len; k++) {
    if (k in O) {
      // 注意 reduce 的核心累加器
      accumulator = callbackfn.call(undefined, accumulator, O[k], O);
    }
  }
  return accumulator;
}

根据上面的代码及注释,有几个关键点需要重点关注:

  1. 初始值默认值不传的特殊处理;

  2. 累加器以及 callbackfn 的处理逻辑。

这两个关键问题处理好,其他的地方和上面几个方法实现的思路是基本类似的

源码

Pop: V8 源码 pop 的实现

Push: V8 源码 push 的实现

Map: V8 源码 map 的实现

Slice: V8 源码 slice 的实现

filter : V8 源码 filter 的实现

相关推荐
陆枫Larry2 分钟前
JavaScript 中的竞态是什么,为啥会有竟态?
前端
DevUI团队3 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
weixin_431600444 小时前
NestJS 入门(3):Guard 如何挡住未登录请求?
前端·后端·学习·nest.js
kyriewen4 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
IT_陈寒4 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端
用户938515635075 小时前
Type vs Interface:读完这篇就没有面试官能难倒你了
前端·面试·typescript
油丶酸萝卜别吃5 小时前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang5 小时前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试
柒和远方6 小时前
V063: TS 面试必考:interface 与 type 的四大差异,与 LLM Harness 的自动化择优
前端·javascript
半个落月6 小时前
React useRef 详解:DOM 引用、持久化值与 Worker 实例
前端·react.js