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

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

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

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

主干

js 复制代码
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 上下文

js 复制代码
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 来处理。

目标: 识别并跳过空位

js 复制代码
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') 这种类数组)?
  • thisnullundefined
js 复制代码
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 确保长度总是一个非负整数,避免负数或奇怪值导致循环出错。

性能优化

  • result.push() 可能会有性能开销,并且无法创建稀疏数组。
  • 规范中创建新数组的方式是 ArraySpeciesCreate,它更复杂但也更精确。
js 复制代码
Array.prototype.myMap = function(callback, thisArg) {
  if (typeof callback !== 'function') {
   throw new TypeError('callback is not a function');
  }
  const O = Object(this);
  const len = O.length >>> 0;

  // 修剪:预分配新数组
  const A = new Array(len);
  let k = 0;

  // 修剪:使用 while 循环
  while (k < len) {
    if (k in O) {
      const kValue = O[k];
      const mappedValue = callback.call(thisArg, kValue, k, O);
      // 修剪:直接赋值
      A[k] = mappedValue;
    }
    k++;
  }
  return A;
};

// 1. 基本功能测试
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']

修剪:

  1. 预分配数组 :用 const A = new Array(len) 提前创建好一个固定长度的数组。
  2. 直接赋值 :在循环中用 A[k] = mappedValue 直接赋值,而不是 push。这样更快,并且如果跳过某个索引,新数组也会在对应位置留下空位。
  3. 使用 while 循环 :规范中用的是 while,我们也跟着用,保持风格一致。

收获总结

参考

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

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

文档原文

相关推荐
Ndmzi4 小时前
Matlab编程技巧:自定义Simulink菜单(理解补充)
前端·javascript·python
勇气要爆发4 小时前
物种起源—JavaScript原型链详解
开发语言·javascript·原型模式
San30.5 小时前
深入理解 JavaScript OOP:从一个「就地编辑组件」看清封装、状态与原型链
开发语言·前端·javascript·ecmascript
AAA阿giao5 小时前
JavaScript 原型与原型链:从零到精通的深度解析
前端·javascript·原型·原型模式·prototype·原型链
0***86336 小时前
SQL Server2019安装步骤+使用+解决部分报错+卸载(超详细 附下载链接)
javascript·数据库·ui
JuneTT6 小时前
【JS】使用内连配置强制引入图片为base64
前端·javascript
代码与野兽7 小时前
AI交易,怎么让LLM自己挑选数据源?
前端·javascript·后端
CC码码7 小时前
前端文本分割工具,“他”来了
前端·javascript·程序员
星火飞码iFlyCode7 小时前
MySQL数据库操作一致性保证(智能对话+AI代码补全案例)【留言有奖】
javascript
韩曙亮7 小时前
【Web APIs】JavaScript 执行机制 ( 单线程特点 | 同步任务与异步任务 | 同步先行、异步排队 | 事件循环机制 )
开发语言·前端·javascript·异步任务·同步任务·web apis·js 引擎