📕 重学JavaScript:如何手写一个`map`高阶函数?

简介: `map` 高阶函数是一个非常常用的数组方法,它可以对数组中的每个元素进行操作,然后返回一个新的数组。

📕 重学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;
}

应该还算实现得完整吧😄。

📕 参考资料

V8源码

Array 原型方法源码实现大揭秘

ecma262草案


🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨

目录
相关文章
|
14天前
|
JavaScript 前端开发 索引
问js的forEach和map的区别
JavaScript中的`forEach`和`map`都是数组迭代方法。`forEach`遍历数组但不修改原数组,无返回值;它接受回调函数处理元素。`map`则遍历数组并返回新数组,新数组元素为回调函数处理后的结果。两者都接收元素、索引和数组作为回调函数参数。
18 7
|
1月前
|
JavaScript 前端开发
解释 JavaScript 中的`map()`、`filter()`和`reduce()`方法的用途。
解释 JavaScript 中的`map()`、`filter()`和`reduce()`方法的用途。
19 1
|
3月前
|
JavaScript 前端开发 定位技术
JavaScript 中如何代理 Set(集合) 和 Map(映射)
JavaScript 中如何代理 Set(集合) 和 Map(映射)
50 0
|
3月前
|
JavaScript 前端开发
JavaScript一种新的数据结构类型Map
JavaScript一种新的数据结构类型Map
|
3月前
|
分布式计算 JavaScript 前端开发
JS中数组22种常用API总结,slice、splice、map、reduce、shift、filter、indexOf......
JS中数组22种常用API总结,slice、splice、map、reduce、shift、filter、indexOf......
|
16天前
|
JavaScript 安全 前端开发
高阶函数(js的问题)
高阶函数(js的问题)
|
26天前
|
存储 JavaScript 索引
js开发:请解释什么是ES6的Map和Set,以及它们与普通对象和数组的区别。
ES6引入了Map和Set数据结构。Map的键可为任意类型,有序且支持get、set、has、delete操作;Set存储唯一值,提供add、delete、has方法。两者皆可迭代。示例展示了Map和Set的基本用法,如添加、查询、删除元素。
13 2
|
1月前
|
存储 JavaScript
JS中Map对象与object的区别
JS中Map对象与object的区别
|
1月前
|
存储 JavaScript 前端开发
javascript中的Map和Set
javascript中的Map和Set
|
3月前
|
JavaScript 前端开发 测试技术
[小笔记]TypeScript/JavaScript模拟Python中的zip(不使用map)
[小笔记]TypeScript/JavaScript模拟Python中的zip(不使用map)
19 0