Appearance
✨ 手写数组方法(map / reduce / filter)👌
要点速览
- 行为一致:遵循规范操作(
ToObject、len >>> 0、k in O)。 - 稀疏数组:空位不调用回调、不写入结果;与
undefined区分。 - this 绑定:使用
callback.call(thisArg, ...)与原生一致。 - reduce 初始值:未传入时,从首个“真实存在”的元素开始;空数组报错。
- 健壮性:调用者不可为
null/undefined,回调必须为函数;兼容类数组。
快速上手
最小可用的三段实现
js
// map:保序、跳过稀疏空位、支持 thisArg
Array.prototype.myMap = function (callback, thisArg) {
if (this == null) throw new TypeError("this is null or not defined");
if (typeof callback !== "function") {
throw new TypeError("callback is not a function");
}
const O = Object(this);
const len = O.length >>> 0;
const result = new Array(len);
for (let k = 0; k < len; k++) {
if (k in O) result[k] = callback.call(thisArg, O[k], k, O);
}
return result;
};
// reduce:区分是否传入初始值;空数组无初始值需抛错
Array.prototype.myReduce = function (callback, initialValue) {
if (this == null) throw new TypeError("this is null or not defined");
if (typeof callback !== "function")
throw new TypeError("callback is not a function");
const O = Object(this);
const len = O.length >>> 0;
let k = 0,
acc;
if (arguments.length >= 2) {
acc = initialValue;
} else {
while (k < len && !(k in O)) k++;
if (k >= len)
throw new TypeError("Reduce of empty array with no initial value");
acc = O[k++];
}
for (; k < len; k++) if (k in O) acc = callback(acc, O[k], k, O);
return acc;
};
// filter:结果数组长度不固定,仅保留回调返回真值的元素
Array.prototype.myFilter = function (callback, thisArg) {
if (this == null) throw new TypeError("this is null or not defined");
if (typeof callback !== "function")
throw new TypeError("callback is not a function");
const O = Object(this);
const len = O.length >>> 0;
const result = [];
for (let k = 0; k < len; k++) {
if (k in O && callback.call(thisArg, O[k], k, O)) result.push(O[k]);
}
return result;
};边界与规范要点
Object(this):兼容类数组调用者(如arguments、NodeList)。len >>> 0:安全取非负整数长度,规避异常length值。k in O:与稀疏数组行为一致,避免将空位误当undefined。thisArg:第二参数作为回调this指向;否则为undefined。- reduce 初始值:不传初始值时,需寻找首个真实元素作为初始累加器。
手写 map 方法详解
常见误区
- 直接
for遍历,不判断k in O,导致对稀疏空位错误调用。 - 忽略
thisArg绑定,使回调中的this与原生行为不一致。 - 未做类型校验:
this为null/undefined或callback非函数时应抛错。
参考实现
javascript
/**
* 模拟原生 Array.prototype.map 行为
* @param {Function} callback - 映射回调函数 (value, index, array) => any
* @param {*} [thisArg] - 指定回调内的 this 指向
* @returns {Array} 新数组,长度与原数组一致,稀疏位置保持为空位
*/
Array.prototype.myMap = function (callback, thisArg) {
// 1) 边界校验:调用者不可为 null/undefined;回调必须为函数
if (this === null || this === undefined) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 2) 将非对象值(原始类型值)转换为对应的包装对象,对于已为对象的值则直接返回其本身。
const O = Object(this); // 相当于 new Object(this),确保调用者为对象
// 3) 安全获取 length:无符号右移,确保为非负整数
const len = O.length >>> 0;
// 4) 预分配结果数组:与原数组等长(map 不修改原数组)
const result = new Array(len);
// 5) 主循环:仅在索引真实存在时调用回调
for (let k = 0; k < len; k++) {
// 使用 in 操作符跳过稀疏空位
if (k in O) {
const value = O[k]; // 当前索引的实际值
// 绑定 thisArg;向回调传递 (value, index, array) 三个标准参数
result[k] = callback.call(thisArg, value, k, O);
}
}
// 6) 返回新数组:保持与原生 map 相同的稀疏结构
return result;
};规范说明:为何执行 Object(this)(ToObject)
- 规范依据:ECMAScript 对多数内置方法都会执行
ToObject内部操作,将非对象值(原始类型值)转换为对应的包装对象,对于已为对象的值则直接返回其本身。。 - 支持类数组:允许在非真正数组的“类数组对象”(如
arguments、NodeList、HTMLCollection)上调用数组方法。 - 原始值包装:当
this是原始类型(如字符串、数字、布尔),会包装成对应的包装对象(如new String(this)),从而可按索引访问其“属性”。 - 稀疏语义一致:对数组与类数组,都用
k in O判断“真实存在的索引”,避免把空位当作undefined。 - 边界提醒:
null/undefined不能被ToObject;因此在此之前需做空值校验并抛出TypeError(与原生一致)。 - 示例:
Array.prototype.myMap.call(arguments, x => x*2)支持类数组参数列表。Array.prototype.myMap.call("abc", ch => ch.toUpperCase())→['A','B','C'](字符串被包装为String对象)。
规范说明:len >>> 0 的作用与原理
- 作用:将
O.length转为无符号 32 位整数(ToUint32),确保长度为非负整数,避免异常值(如小数、NaN、Infinity、字符串)影响遍历。 - 原理:位运算操作数在执行前会进行
ToUint32转换;>>> 0等价于“取该值的 32 位无符号整数表示”。 - 为什么需要:数组方法实现需要使用“规范化的整数长度”,以与原生行为保持一致(近似
ToLength的效果)。 - 行为示例:
"5" >>> 0→5(字符串被转为数字)3.7 >>> 0→3(截断为整数)NaN >>> 0→0Infinity >>> 0→0-1 >>> 0→4294967295(无符号 32 位的表示;真实数组不会出现负length)
- 与规范的关系:ECMAScript 的
ToLength上限为2^53 − 1,而>>> 0上限为2^32 − 1;数组的合法length最大为2^32 − 1,因此在数组方法中两者表现一致。
map 行为要点
- 空值校验:调用者不可为
null/undefined;callback必须为函数。 - ToObject:使用
Object(this)支持原始数据和类数组(如arguments、NodeList)。 - 长度规范化:
len = O.length >>> 0取无符号 32 位整数,保证非负。 - 稀疏数组:用
k in O跳过空位,保持与原生map一致的稀疏语义。 - this 绑定:通过
callback.call(thisArg, ...)保持与原生一致的this指向。 - 复杂度:时间
O(n),空间O(n)(返回新数组)。
行为一致性自测
js
// 1) 稀疏数组:原生 map 不处理空位
const a = [, 2, , 4];
console.log(
a.map((x) => x),
a.myMap((x) => x)
); // [empty, 2, empty, 4]
// 2) thisArg:保持与原生一致
const ctx = { mul: 3 };
console.log(
[1, 2].myMap(function (x) {
return x * this.mul;
}, ctx)
); // [3,6]
// 3) 类数组:NodeList/arguments 等
function demo() {
return Array.prototype.myMap.call(arguments, (x) => x * 2);
}
console.log(demo(1, 2, 3)); // [2,4,6]手写 reduce 方法详解
核心难点
- 初始值处理:有/无初始值两种分支;空数组且无初始值必须抛错。
- 稀疏数组跳过空位:寻找“首个真实存在的元素”作为初始值。
参考实现
javascript
/**
* 模拟原生 Array.prototype.reduce 行为
* @param {Function} callback - 规约回调 (accumulator, currentValue, index, array) => any
* @param {*} [initialValue] - 累加器初始值;未传时以“首个真实存在元素”为初值
*/
Array.prototype.myReduce = function (callback, initialValue) {
// 1) 边界校验:this 不可为 null/undefined
if (this === null || this === undefined) {
throw new TypeError("this is null or not defined");
}
// 2) 回调类型校验:callback 必须为函数
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 3) ToObject:兼容原始数据和类数组调用者
const O = Object(this);
// 4) 规范化长度:确保为非负整数
const len = O.length >>> 0;
let accumulator;
let k = 0;
// 5) 是否传入初始值:两种分支处理
if (arguments.length >= 2) {
accumulator = initialValue;
} else {
// 未传初始值:从数组中找到首个“真实存在”的元素
while (k < len && !(k in O)) k++;
// 空数组且无初始值:必须抛错(与原生一致)
if (k >= len)
throw new TypeError("Reduce of empty array with no initial value");
accumulator = O[k++];
}
// 6) 主循环:跳过空位,依次规约更新累加器
while (k < len) {
if (k in O) {
accumulator = callback.call(undefined, accumulator, O[k], k, O);
}
}
return accumulator;
};reduce 行为要点
- 空值校验:调用者不可为
null/undefined;callback必须为函数。 - ToObject:使用
Object(this)支持原始数据类数组(如arguments、NodeList)。 - 长度规范化:
len = O.length >>> 0取无符号 32 位整数,保证非负。 - 稀疏数组:用
k in O跳过空位,与原生reduce行为一致。 - 初始值分支:未传初始值时,寻找第一个“真实存在”的元素;空数组且无初始值必须抛错。
- 复杂度:时间
O(n),空间O(1)。
行为一致性自测
js
console.log([1, 2, 3].myReduce((a, b) => a + b, 0)); // 6
console.log([, 2, , 4].myReduce((a, b) => a + b)); // 6(首个真实元素为 2)
try {
[].myReduce((a, b) => a + b);
} catch (e) {
console.log(e.message);
}手写 filter 方法详解
参考实现
javascript
/**
* 模拟原生 Array.prototype.filter 行为
* @param {Function} callback - 筛选回调 (element, index, array) => boolean
* @param {*} [thisArg] - 回调中的 this 指向
*/
Array.prototype.myFilter = function (callback, thisArg) {
// 1) 边界校验:this 不可为 null/undefined
if (this === null || this === undefined) {
throw new TypeError("this is null or not defined");
}
// 2) 回调类型校验:callback 必须为函数
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 3) ToObject:兼容原始数据和类数组调用者
const O = Object(this);
// 4) 规范化长度:确保为非负整数
const len = O.length >>> 0;
const result = [];
for (let k = 0; k < len; k++) {
// 5) 跳过空位:仅对真实存在的索引调用回调
if (k in O) {
const v = O[k];
// 6) 调用回调:仅当回调返回真值时,将当前元素 push 到结果数组
if (callback.call(thisArg, v, k, O)) result.push(v);
}
}
return result;
};filter 行为要点
- 空值校验:调用者不可为
null/undefined;callback必须为函数。 - ToObject:使用
Object(this)支持原始数据和类数组调用者。 - 长度规范化:
len = O.length >>> 0保证非负整数长度。 - 稀疏数组:用
k in O跳过空位,不将空位视为undefined。 - 真值保留:仅当回调返回真值时,将当前元素
push到结果数组。 - this 绑定:使用
callback.call(thisArg, ...)绑定回调中的this。 - 复杂度:时间
O(n),空间O(n)(结果数组大小由命中次数决定)。
行为一致性自测
js
console.log([1, 2, 3, 4].myFilter((x) => x % 2 === 0)); // [2,4]
console.log([, 2, , 4].myFilter(() => true)); // [2,4](空位被跳过)常见误区与解决方案
常见误区
- 将稀疏数组空位当作
undefined处理,导致与原生不一致。 - 忽略
thisArg,使回调this指向错误。 - 未做
callback类型校验或this空值校验,规范性不足。 - reduce 对空数组无初始值不抛错,违反原生行为。
| 误区场景 | 问题原因 | 解决方案 |
|---|---|---|
| 稀疏数组被错误遍历 | 未使用 k in O 判断真实存在的索引 | 用 in 操作符跳过空位 |
| 回调 this 指向不一致 | 回调直接调用,未绑定 thisArg | 使用 callback.call(thisArg, ...) |
| reduce 初始值处理错 | 不区分是否传入初始值、空数组未抛错 | 按规范分支逻辑处理,空数组无初始值必须抛错 |
进阶优化:惰性求值与 Transducer
背景与动机
- 链式调用的中间数组开销:
arr.map().filter().reduce()每步都会创建中间数组,导致额外的内存分配与 GC 压力,尤其是大数据量或高频调用的“热路径”。 - 性能影响:中间数组不仅占用内存,还增加 CPU 拷贝与遍历次数,放大端到端延迟;在前端渲染(如列表、图表)与后端数据处理(日志、ETL)中尤为明显。
- 流式/迭代数据:数据源常以“流/迭代器”形式提供(事件流、读取器、网络数据),不适合一次性构建多个中间集合;更需要“边遍历边处理”的管道式转换。
- 惰性求值(迭代器/生成器):将转换延后到真正消费时执行,按需产出元素,天然消除中间数组,实现“只遍历一次”的流水线。
- Transducer 模式:将多个转换(如
map+filter)合成为一个“高阶 reducer”,一次遍历即可完成所有转换,适配任意聚合器(push、concat、sum),无中间集合。 - 典型适用场景:
- 前端:长列表渲染、图形数据预处理、动画帧内数据管道(减少主线程压力)
- 后端/Node:日志处理、CSV/JSON 流式解析、消息队列消费者(提高吞吐与降低内存峰值)
惰性管道(生成器示例)
js
function* map(iter, fn) {
for (const x of iter) yield fn(x);
}
function* filter(iter, pred) {
for (const x of iter) if (pred(x)) yield x;
}
const data = [1, 2, 3, 4, 5];
const pipeline = filter(
map(data, (x) => x * 2),
(x) => x % 3 === 0
);
console.log(Array.from(pipeline)); // [6]Transducer 思路(仅遍历一次)
js
const mapping = (fn) => (reducer) => (acc, x) => reducer(acc, fn(x));
const filtering = (pred) => (reducer) => (acc, x) =>
pred(x) ? reducer(acc, x) : acc;
const compose = (...fns) => fns.reduce((a, b) => (x) => a(b(x)));
const xf = compose(
mapping((x) => x * 2),
filtering((x) => x % 3 === 0)
);
const reducer = (acc, x) => {
acc.push(x);
return acc;
};
const result = [1, 2, 3, 4, 5].reduce(xf(reducer), []);
console.log(result); // [6]面试答题核心要点
- 明确规范:
ToObject、len >>> 0、k in O、thisArg、reduce 初始值分支。 - 展示一致性:提供稀疏数组、this 绑定、类数组的对比自测示例。
- 体现工程思维:提及惰性求值与 Transducer,消除中间数组开销。
小结与后续
- 手写方法的关键在“与原生行为一致”,先界定规范点再实现。
- 自测用例覆盖稀疏数组、this 绑定、类数组、reduce 初始值四类场景。
- 进一步可补充
forEach、some、every的实现与一致性测试,形成方法族。
