📕 重学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草案


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

目录
相关文章
|
4月前
|
存储 JavaScript 前端开发
js中map属性
js中map属性
50 1
|
6月前
|
Python
高阶函数如`map`, `filter`, `reduce`和`functools.partial`在Python中用于函数操作
【6月更文挑战第20天】高阶函数如`map`, `filter`, `reduce`和`functools.partial`在Python中用于函数操作。装饰器如`@timer`接收或返回函数,用于扩展功能,如记录执行时间。`timer`装饰器通过包裹函数并计算执行间隙展示时间消耗,如`my_function(2)`执行耗时2秒。
36 3
|
6月前
|
存储 JavaScript 前端开发
JavaScript进阶-Map与Set集合
【6月更文挑战第20天】JavaScript的ES6引入了`Map`和`Set`,它们是高效处理集合数据的工具。`Map`允许任何类型的键,提供唯一键值对;`Set`存储唯一值。使用`Map`时,注意键可以非字符串,用`has`检查键存在。`Set`常用于数组去重,如`[...new Set(array)]`。了解它们的高级应用,如结构转换和高效查询,能提升代码质量。别忘了`WeakMap`用于弱引用键,防止内存泄漏。实践使用以加深理解。
83 3
|
3月前
|
存储 JavaScript 前端开发
js的map和set |21
js的map和set |21
|
3月前
|
JSON JavaScript 前端开发
JavaScript第五天(函数,this,严格模式,高阶函数,闭包,递归,正则,ES6)高级
JavaScript第五天(函数,this,严格模式,高阶函数,闭包,递归,正则,ES6)高级
|
3月前
|
JavaScript 前端开发
js map和reduce
js map和reduce
|
2月前
|
存储 JavaScript 前端开发
js中map属性
js中map属性
22 0
|
2月前
|
前端开发 JavaScript 索引
JavaScript 数组常用高阶函数总结,包括插入,删除,更新,反转,排序等,如map、splice等
JavaScript数组的常用高阶函数,包括遍历、插入、删除、更新、反转和排序等操作,如map、splice、push、pop、reverse等。
19 0
|
3月前
|
JavaScript 前端开发
JavaScript Array map() 方法
JavaScript Array map() 方法
|
3月前
|
JavaScript 前端开发
JavaScript 中 五种迭代数组的方法 every some map filter forEach
本文介绍了JavaScript中五种常用数组迭代方法:every、some、filter、map和forEach,并通过示例代码展示了它们的基本用法和区别。