我正在参加「掘金·启航计划」
起因
最近翻阅corejs源码,在看every的逻辑实现时,发现corejs将forEach、map、filter、some、every、find、findIndex的实现使用一个工厂函数实现了,只需要传入不同的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;
接下来就是对整个数组的遍历了,整个流程大概是这样的:
- 遍历数组,若当前下标在数组中或
NO_HOLES为true,则执行以下逻辑,否则跳出本次循环,继续遍历 - 获取每个数组元素的值
value,并调用回调函数返回结果res - 若要模拟
map,则直接将回调的每个值存储到target对应下标中 - 否则判断
res是否有值或为真,进行以下逻辑 - 如果模拟
filter,则将respush到target中,和map不同,filter只会返回符合条件的值组成的数组。 - 如果是模拟
some,则遇到res为真值就直接返回true,遍历结束。 - 如果是模拟
find,则返回对应的value - 如果是模拟
find,则返回对应的index - 如果
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仅在模拟find和findIndex是为真,通过测试原生方法我们发现只有find和findIndex会遍历整个数组长度,无论下标是否在数组中:
故使用NO_HOLES进行这方面逻辑的判断。
循环结束之后若代码继续执行,则说明有以下几个情况:
- 没有找到,返回undefined
- some返回true
- every返回true
- filter返回一个新数组
- map返回一个新数组
- 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