从V8源码角度来手撕JS数组方法底层实现(二):map

1,691 阅读3分钟

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

前言

map方法是日常开发经常用到的方法之一,调用后返回一个新数组,新数组的元素是原数组中的每个元素是调用一次提供的函数后的返回值。

函数签名:

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
 // Return element for new_array 
}[, thisArg])
  • callback:生成新数组元素的函数,包含三个参数:
    • currentValue: 正在处理的当前元素。
    • index: 当前元素的索引。
    • array:map 方法调用的数组。
  • thisArg:可选,执行callback函数时绑定的this

手写实现

先来看下ECMA规范中描述的函数调用过程:

image.png

  1. 使用Object()来将this变成对象变量表示。
  2. 初始化数组的length,也就是对length值进行合法化的处理,它必须是一个有效范围内的整数。
  3. 判断是否存在回调函数callback,不存在则抛出一个TypeError异常。
  4. 创建一个与数组长度相同的数组,存储回调函数处理结果和返回值
  5. 以索引0开头,升序遍历数组的每个元素,遇到空值会直接跳过,不执行回调函数,但是新的数组上这个位置也会是一个空值,也就是说新数组保持了和原数组一样的稀疏结构。
  6. 遍历处理完后,返回结果数组。
Array.prototype._map = function(callbackFn, thisArg) {
  // 判断this值是否合法,否则抛出异常
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'map' of null");
  }
  // 判断回调函数是否合法
  if (Object.prototype.toString.call(callbackFn) != "[object Function]") {
    throw new TypeError(callbackFn + ' is not a function')
  }
  // 将this值具象化成变量O
  let O = Object(this);
  let receiver = thisArg;
  // 使用无符号右移运算符对length值右移0位,相当于向下取整,丢弃小数部分
  let len = O.length >>> 0;
  // 创建一个长度相同的结果数组
  let A = new Array(len);
  for(let k = 0; k < len; k++) {
    // 以0-length的顺序处理元素,跳过空值
    if (k in O) {
      let kValue = O[k];
      // 依次传入this, 当前项,当前索引,整个数组
      let mappedValue = callbackFn.call(receiver, kValue, k, O);
      A[k] = mappedValue;
    }
  }
  // 返回结果
  return A;
}

这里要特别注意的是,map遍历会跳过空值而不进行回调函数处理,但是对应到新数组上跳过的位置也会是空值,它会保持和原数组一致的稀疏结构,对于回调函数,如果回调函数没有返回值,那么对应新数组的这个位置会是undefined

v8实现

v8/array.js中map实现

function ArrayMap(f, receiver) {
  // CHECK_OBJECT_COERCIBLE函数对this进行检查,判断其是否可以具象化为对象变量,不行则抛出异常
  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.
  
  // this具象化为对象变量,也就是this指向的数组本身
  var array = TO_OBJECT(this);
  // length合法性处理
  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总体上是一致的。

总结

map 的实现不会太难,基本就是再多加一些判断,循环遍历实现 map 的思路,将处理过后的 mappedValue 赋给一个新定义的数组 A,最后返回这个新数组 A,并不改变原数组的值。