📕 重学JavaScript:如何手写一个map
高阶函数?
嗨,大家好!这里是道长王jj
~ 🎩🧙♂️
map
高阶函数是一个非常常用的数组方法,它可以对数组中的每个元素进行操作,然后返回一个新的数组。
map函数可以让我们用一种简洁和优雅的方式来处理数组数据,比如映射、转换、过滤等等。🏭
我们今天想要实现这个目标😊,肯定不可能凭空去猜测map的内部逻辑。这样进步太慢了😅
其实在ECMA中,他们已经给我们解释了map方法的设计原理:
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(𝔽(k)).
b. Let kPresent be ? HasProperty(O, Pk).
c. If kPresent is true, then
i. Let kValue be ? Get(O, Pk).
ii. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
d. Set k to k + 1.
7. Return A.
这段伪代码看起来好像还很复杂的?我们一步一步来解析一下:
- 把调用map函数的值转换成一个对象(ToObject)
- 获取对象的长度(LengthOfArrayLike)
- 判断传入的回调函数是否是一个函数(IsCallable)
- 创建一个新的数组(ArraySpeciesCreate)
- 遍历对象中的每个元素(Repeat)
- 判断元素是否存在(HasProperty)
- 获取元素的值(Get)
- 调用回调函数对元素进行操作(Call)
- 把操作后的值放到新数组中(CreateDataPropertyOrThrow)
- 返回新数组(Return)
好的,根据这个原理,我们可以用JavaScript来实现我们的目标。😎
⭕ 解答一部分实现的关键逻辑
把调用 map 函数的值转换成一个对象(ToObject)
保证 map 函数可以接受任何类型的值作为参数,比如字符串、数字、布尔等,不会因为用户传入的差异导致报错。
let O = Object(true);
O.length // undefind
let O = Object([1,2,3]);
O.length // 3
let O = Object('1234');
O.length // 4
获取对象的长度(LengthOfArrayLike)
确定我们需要遍历对象中的多少个元素
我们使用 >>> 0 运算符来转换对象的 length 属性 ,如果对象没有 length 属性,或者 length 属性不是一个有效的数字,那么 len 的值就是 0
let len = O.length >>> 0
如果你还不是很了解>>>
,我这里举了很多例子,它会告诉你不同的东西用这个符号之后会变成什么样子。👇
null >>> 0 //0 (什么都没有变成0)
undefined >>> 0 //0 (不知道是什么也变成0)
void(0) >>> 0 //0 (空空如也还是0)
function a (){
}; a >>> 0 //0 (一个函数也变成0)
[] >>> 0 //0 (一个空的列表也变成0)
var a = {
}; a >>> 0 //0 (一个空的对象也变成0)
123123 >>> 0 //123123 (一个整数不会变)
45.2 >>> 0 //45 (一个小数会去掉小数点后面的部分)
0 >>> 0 //0 (本来就是0还是0)
-0 >>> 0 //0 (负数的0也还是0)
-1 >>> 0 //4294967295 (负数会变成一个很大的数)
-1212 >>> 0 //4294966084 (负数会变成一个很大的数)
判断传入的回调函数是否是一个函数(IsCallable)
保证我们传入的回调函数是一个有效的函数,而不是其他类型的值
我们使用 typeof 运算符来判断回调函数的类型是否是 "function"
如果不是,我们就抛出一个类型错误
if (typeof callbackfn !== "function") {
throw new TypeError(callbackfn + " is not a function");
}
调用回调函数对元素进行操作(Call)
对当前索引对应的元素进行操作,并且得到一个新的值
callbackfn.call(thisArg, kValue, k, O);
💌 完整代码解析如下:
// 定义一个 map 函数,接受两个参数,一个是回调函数,一个是回调函数的 this 值(可选)
function map(callbackfn, thisArg) {
// 把调用 map 函数的值转换成一个对象(ToObject)
// 这一步是为了保证 map 函数可以接受任何类型的值作为参数,比如字符串、数字、布尔等
// 我们使用 Object 构造函数来把任何类型的值转换成一个对象
let O = Object(this);
// 获取对象的长度(LengthOfArrayLike)
// 这一步是为了确定我们需要遍历对象中的多少个元素
// 我们使用 >>> 0 运算符来把对象的 length 属性转换成一个无符号的 32 位整数
// 如果对象没有 length 属性,或者 length 属性不是一个有效的数字,那么 len 的值就是 0
let len = O.length >>> 0;
// 判断传入的回调函数是否是一个函数(IsCallable)
// 这一步是为了保证我们传入的回调函数是一个有效的函数,而不是其他类型的值
// 我们使用 typeof 运算符来判断回调函数的类型是否是 "function"
// 如果不是,我们就抛出一个类型错误
if (typeof callbackfn !== "function") {
throw new TypeError(callbackfn + " is not a function");
}
// 创建一个新的数组(ArraySpeciesCreate)
// 这一步是为了创建一个新的数组来存放我们操作后的元素
// 我们使用 Array 构造函数来创建一个新的数组,并且指定它的长度和原对象相同
let A = new Array(len);
// 遍历对象中的每个元素(Repeat)
// 这一步是为了对对象中的每个元素进行操作,并且把操作后的结果放到新数组中
// 我们使用 for 循环来遍历对象中的每个元素,从索引为 0 的元素开始,直到索引为 len - 1 的元素结束
for (let k = 0; k < len; k++) {
// 判断元素是否存在(HasProperty)
// 这一步是为了判断当前索引对应的元素是否存在于对象中
// 我们使用 in 运算符来判断对象是否有当前索引对应的属性
// 如果有,我们就继续进行下面的操作,如果没有,我们就跳过这个元素
if (k in O) {
// 获取元素的值(Get)
// 这一步是为了获取当前索引对应的元素的值
// 我们使用 [] 运算符来从对象中获取当前索引对应的属性的值,并且赋值给一个变量 kValue
let kValue = O[k];
// 调用回调函数对元素进行操作(Call)
// 这一步是为了对当前索引对应的元素进行操作,并且得到一个新的值
// 我们使用 call 方法来调用回调函数,并且传入三个参数:当前元素、当前索引、原对象,并且绑定 this 值
// 我们把回调函数返回的值赋值给一个变量 mappedValue
let mappedValue = callbackfn.call(thisArg, kValue, k, O);
// 把操作后的值放到新数组中(CreateDataPropertyOrThrow)
// 这一步是为了把操作后的值放到新数组中,并且保证新数组和原对象有相同的长度和顺序
// 我们使用 [] 运算符来给新数组中的当前索引对应的属性赋值为操作后的值
// 如果赋值失败,我们就抛出一个错误
A[k] = mappedValue;
}
}
// 返回新数组(Return)
// 这一步是为了返回我们创建的新数组,作为 map 函数的结果
// 我们使用 return 语句来返回新数组
return A;
}
总体实现起来并没那么难,需要注意的就是使用 in 来进行原型链查找。同时,如果没有找到就不处理,能有效处理稀疏数组的情况。
💨 我们试一下是否能实现map函数的效果
let nums = [1, 2, 3];
let double = function(x) {
return x * 2;
};
let result = map.call(nums, double);
console.log(result); // [2, 4, 6]
✔ V8引擎中的map源码是怎么实现的呢?
我在这里直接粘贴给大家看看,大家自行对比一下:
function ArrayMap(f, receiver) {
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");
// Pull out the length so that modifications to the length in the
// loop will not affect the looping and side effects are visible.
var array = TO_OBJECT(this);
var length = TO_LENGTH(array.length);
if (!IS_CALLABLE(f)) throw %make_type_error(kCalledNonCallable, f);
var result = ArraySpeciesCreate(array, length);
for (var i = 0; i < length; i++) {
if (i in array) {
var element = array[i];
%CreateDataProperty(result, i, %_Call(f, receiver, element, i, array));
}
}
return result;
}
应该还算实现得完整吧😄。
📕 参考资料
🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨