手写forEach、map、filter……?一个函数搞定!

109 阅读4分钟

我正在参加「掘金·启航计划」

起因

最近翻阅corejs源码,在看every的逻辑实现时,发现corejs将forEachmapfiltersomeeveryfindfindIndex的实现使用一个工厂函数实现了,只需要传入不同的TYPE值,即可完成对响应函数的模拟操作,话不多说,让我们一起看一下它的实现吧!

分析

既然能够使用一个函数实现,那么这七个函数之间一定有相似之处,相信不少小伙伴应该已经发现了,他们运行的背后都需要去遍历当前的数组,并通过回调函数来确定接下来如何去执行及返回。通过查阅MDN我们发现,他们的参数都是一样的:

  • callbackFn

  • 回调函数对于不同的数组原型方法有不同的使用方式,但接受的参数都相同:

    • element

      数组中当前正在处理的元素。

    • index

      正在处理的元素在数组中的索引。

    • array

      调用了 filter() 的数组本身。

  • thisArg可选

  • 执行 callbackFn 时,用于 this 的值。

代码

1. 类型定义

既然要一个函数实现多个数组方法,首先就需要定义类型,让函数根据类型去做不同的操作:

  const IS_MAP = TYPE === 1; //Array.prototype.map
  const IS_FILTER = TYPE === 2; //Array.prototype.filter
  const IS_SOME = TYPE === 3; //Array.prototype.some
  const IS_EVERY = TYPE === 4; //Array.prototype.every
  const IS_FIND_INDEX = TYPE === 6; //Array.prototype.findIndex
  const NO_HOLES = TYPE === 5 || IS_FIND_INDEX;

至于NO_HOLES我们先按下不表。

2. 核心方法

定义好类型后,我们就可以返回一个函数,函数里面是代码的核心逻辑,且根据不同的类型做出不同操作。

我们要获取当前的数组元素,如果要模拟原生方法,那么当前数组元素应该是this,即:

const arr = this; //获取数组元素

thisArg参数需要绑定到callbackFn上,用于绑定this值:

callBackFn = callBackFn.bind(
      typeof thisArg === "undefined" ? window : thisArg
    ); //若thisArg为空则绑定到window对象上

还需要定义一个target变量,用于最终结果的返回

如果是map则长度为原数组长度的数组,如果是filter则为长度0的数组,否则设置为undefined

const target = IS_MAP ? new Array(length) : IS_FILTER ? [] : undefined;

接下来就是对整个数组的遍历了,整个流程大概是这样的:

  1. 遍历数组,若当前下标在数组中或NO_HOLEStrue,则执行以下逻辑,否则跳出本次循环,继续遍历
  2. 获取每个数组元素的值value,并调用回调函数返回结果res
  3. 若要模拟map,则直接将回调的每个值存储到target对应下标中
  4. 否则判断res是否有值或为真,进行以下逻辑
  5. 如果模拟filter,则将respush到target中,和map不同,filter只会返回符合条件的值组成的数组。
  6. 如果是模拟some,则遇到res为真值就直接返回true,遍历结束。
  7. 如果是模拟find,则返回对应的value
  8. 如果是模拟find,则返回对应的index
  9. 如果res为假且模拟的为every,则返回false。

具体代码如下:

for (; length > index; index++) {
  if (NO_HOLES || index in arr) {
    {
      const value = arr[index];
      const res = callBackFn(value, index, arr);
      if (TYPE) {
        if (IS_MAP) target[index] = res;
        // 如果res有值或是true则继续往下执行
        else if (res) {
          switch (TYPE) {
            case 2:
              target.push(value);
              break; // filter
            case 3:
              return true; // some
            case 5:
              return value; // find
            case 6:
              return index; // findIndex
          }
        } else if (IS_EVERY) {
          // 如果是every且res是false,则返回false
          return false;
        }
      }
    }
  }
}

这里讲一下NO_HOLES,在前面代码中我们可以知道NO_HOLES仅在模拟findfindIndex是为真,通过测试原生方法我们发现只有findfindIndex会遍历整个数组长度,无论下标是否在数组中:

image.png

故使用NO_HOLES进行这方面逻辑的判断。 循环结束之后若代码继续执行,则说明有以下几个情况:

  1. 没有找到,返回undefined
  2. some返回true
  3. every返回true
  4. filter返回一个新数组
  5. map返回一个新数组
  6. findIndex返回-1

代码如下:

return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target;

至此,整个函数逻辑基本介绍完毕,全部代码如下:

function createMethod(TYPE) {
  const IS_MAP = TYPE === 1; //Array.prototype.map
  const IS_FILTER = TYPE === 2; //Array.prototype.filter
  const IS_SOME = TYPE === 3; //Array.prototype.some
  const IS_EVERY = TYPE === 4; //Array.prototype.every
  const IS_FIND_INDEX = TYPE === 6; //Array.prototype.findIndex
  const NO_HOLES = TYPE === 5 || IS_FIND_INDEX;
  return function (callBackFn, thisArg) {
    const arr = this;
    callBackFn = callBackFn.bind(
      typeof thisArg === "undefined" ? window : thisArg
    );
    let length = arr.length;
    let index = 0;
    // 创建一个新数组,如果是map则长度为原数组长度,如果是filter则为0
    const target = IS_MAP ? new Array(length) : IS_FILTER ? [] : undefined;
    for (; length > index; index++) {
      if (NO_HOLES || index in arr) {
        {
          const value = arr[index];
          const res = callBackFn(value, index, arr);
          if (TYPE) {
            if (IS_MAP) target[index] = res;
            // 如果res有值或是true则继续往下执行
            else if (res) {
              switch (TYPE) {
                case 2:
                  target.push(value);
                  break; // filter
                case 3:
                  return true; // some
                case 5:
                  return value; // find
                case 6:
                  return index; // findIndex
              }
            } else if (IS_EVERY) {
              // 如果是every且res是false,则返回false
              return false;
            }
          }
        }
      }
    }
    // 走到这有几种情况
    // 1. 没有找到,返回undefined
    // 2. some返回true
    // 3. every返回true
    // 4. filter返回一个新数组
    // 5. map返回一个新数组
    // 6. findIndex返回-1
    return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target;
    //                     6                            2/3       1/4/5
  };
}

接下来我们可以创建数组这些原生方法了:

const methods = {
  forEach: createMethod(0),
  map: createMethod(1),
  filter: createMethod(2),
  some: createMethod(3),
  every: createMethod(4),
  find: createMethod(5),
  findIndex: createMethod(6),
};

3. 测试

console.log(methods.map.call(testArr, (item) => item * 2)); //[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
console.log(methods.filter.call(testArr, (item) => item % 2 === 0)); //[2, 4, 6, 8, 10]
console.log(methods.some.call(testArr, (item) => item % 2 === 0)); //true
console.log(methods.every.call(testArr, (item) => item > 0)); //true
console.log(methods.find.call(testArr, (item) => item === 5)); //5
console.log(methods.findIndex.call(testArr, (item) => item === 5)); //4