不止于会用:跟着 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;
};
变化:
- 在函数签名中接收
thisArg参数。 - 调用回调时,不用
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;
};
变化:
- 在循环中,访问元素前,先检查这个索引位置上是否真的有值。
- JavaScript 的
in操作符:if (i in this)。
防御性编程
callback不是一个函数?- 在非数组上调用(比如
document.getElementsByTagName('div')这种类数组)? this是null或undefined?
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;
}
修剪:
- 检查
callback:在函数开头if (typeof callback !== 'function'),如果不是函数就立刻报错。 - 转换
this:使用const O = Object(this)确保this总是一个对象,这样即使在字符串hello上调用,也能把它变成一个类数组对象{0: 'h', 1: 'e', ...}来处理。 - 安全获取长度 :使用
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']
修剪:
- 预分配数组 :用
const A = new Array(len)提前创建好一个固定长度的数组。 - 直接赋值 :在循环中用
A[k] = mappedValue直接赋值,而不是push。这样更快,并且如果跳过某个索引,新数组也会在对应位置留下空位。 - 使用
while循环 :规范中用的是while,我们也跟着用,保持风格一致。
收获总结
参考
文档地址:ECMAScript® 2023 Language Specification - Array.prototype.map
文档原文


