跟着ECMAScript 规范,手写数组方法之map

71 阅读2分钟

不止于会用:跟着 ECMAScript 规范,手写 map

开宗明义:以传入的数组,回调函数 和 thisArg 加工成新数组。

Array.prototype.myMap 接受一个可调用的 callback 和可选的 thisArg,按从 0 到 ToLength(O.length)-1 的索引依次对传入的(类)数组元素调用 callback(参数为 currentValue, index, 原对象),把每次调用的返回值放到同一索引的新数组并返回该新数组;回调不会改变原数组,也不会为原数组中“空位”(hole)创建值。

主干

Array.prototype.myMap = function(callback) {
    // 创建空数组
  const result = [];

  // this即传入数组
  for (let i = 0; i < this.length; i++) {
    result.push(callback(this[i], i, this));
  }

  return result;
};


树枝

考虑到边界的处理

处理 thisArg

如果想在回调函数里使用一个特定的 this 上下文

Array.prototype.myMap = function(callback, thisArg) {
  const result = [];
  for (let i = 0; i < this.length; i++) {
    result.push(callback.call(thisArg, this[i], i, this));
  }
  return result;
};

变化:

  1. 在函数签名中接收 thisArg 参数。
  2. 调用回调时,不用 callback(),而是用 callback.call(thisArg) 来手动指定 this

处理稀疏数组

原生的 map 会返回 [2, <1 empty item>, 4],它会跳过空位。主干版本没有这个能力,它会把空位当成 undefined 来处理。

目标: 识别并跳过空位

Array.prototype.customMap = function(callback, thisArg) {
  const result = [];
  for (let i = 0; i < this.length; i++) {
    if (i in this) {
      result.push(callback.call(thisArg, this[i], i, this));
    }
  }
  return result;
};

变化:

  1. 在循环中,访问元素前,先检查这个索引位置上是否真的有值。
  2. JavaScript 的 in 操作符:if (i in this)

防御性编程

  • callback 不是一个函数?
  • 在非数组上调用(比如 document.getElementsByTagName('div') 这种类数组)?
  • this 是 null 或 undefined
Array.prototype.customMap = function(callback, thisArg) {
    // 修剪1:检查 callback
  if (typeof callback !== 'function') {
    throw new TypeError('callback is not a function');
  }
    // 修剪2: 包装类
  const O = Object(this);
    // 修剪3: 安全长度
  const len = O.length >>> 0;

  const result = [];
  for (let i = 0; i < len; i++) {
    if (i in O) {
      result.push(callback.call(thisArg, O[i], i, O));
    }
  }
  return result;
}

修剪:

  1. 检查 callback:在函数开头 if (typeof callback !== 'function'),如果不是函数就立刻报错。
  2. 转换 this:使用 const O = Object(this) 确保 this 总是一个对象,这样即使在字符串 hello 上调用,也能把它变成一个类数组对象 {0: 'h', 1: 'e', ...} 来处理。
  3. 安全获取长度:使用 const len = O.length >>> 0 确保长度总是一个非负整数,避免负数或奇怪值导致循环出错。

测试

Array.prototype.myMap = function(callback, thisArg) {
 基本功能测试
console.log([1, 2, 3].myMap(x => x * 2)); // [2, 4, 6]

// 2. 稀疏数组测试
const sparse = [1, , 3];
console.log(sparse.myMap(x => x + 1)); // [2, <1 empty item>, 4] (正确跳过空位)

// 3. thisArg 测试
const multiplier = { factor: 10 };
function multiply(item) {
  return item * this.factor;
}
console.log([1, 2, 3].myMap(multiply, multiplier)); // [10, 20, 30]

// 4. 类数组对象测试
function f() {
  return Array.prototype.myMap.call(arguments, x => x + 1);
}
console.log(f(4, 5, 6)); // [5, 6, 7]

// 5. 字符串测试
console.log(Array.prototype.myMap.call('abc', char => char.toUpperCase())); // ['A', 'B', 'C']

参考

文档地址:ECMAScript® 2023 Language Specification - Array.prototype.map

【JavaScript】大厂前端面试官最爱考的JS手写题整理出来了!你能答对几道题?_哔哩哔哩_bilibili

文档原文

001_map.png

002_map.png

003_map.png