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

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

  我们都知道,比较常用的数组方法有 pushpopslicemapreduce等,

围绕这几个常用方法,并结合 V8的源代码手写这些方法的底层实现。

为了方便更好地理解,在开始前先回想一下:

  1. reduce方法里面的参数都是什么作用?
  2. pushpop的底层逻辑是什么样的呢?

push 方法的底层实现

  ECMA 的官网关于 push的基本描述:

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).

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

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 方法的底层实现

  ECMA 官网关于 pop的基本描述:

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.

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

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,需要做一下特殊处理。

map 方法的底层实现

  ECMA 的官网关于 map的基本描述:

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.
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;
}

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

reduce 方法的底层实现

 ECMA 的官网关于 reduce的基本描述:

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,
    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.
10. Return accumulator.

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的处理逻辑。

总结

  到这里,本文的内容就先告一段落了。这一篇内容虽少,但却是必须要掌握的内容。再贴一下 V8数组关于各种方法的实现源码地址,如下表所示。

数组方法V8 源码地址
popV8 源码 pop 的实现
pushV8 源码 push 的实现
mapV8 源码 map 的实现
sliceV8 源码 slice 的实现
filterV8 源码 filter 的实现
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值