手写 map 与 reduce,如何保留空位并处理未提供初始值?
先理解原理和示例,再练习完整口述与追问。
回答要点
固定初始长度;用 in 检查属性存在;回调签名正确;reduce 根据 arguments.length 判断初始值;不修改内置原型。
原理拆解
下面提供独立的 arrayMap 和 arrayReduce,不覆盖 Array.prototype。它们展示普通数组及 array-like 对象的核心行为:开始时快照长度、跳过不存在的索引、访问继承索引、向回调传入原对象。不是完整规范 polyfill,不实现 Array 子类 species 等高级细节;map 结果仍受 JavaScript 数组长度上限约束,不应对任意巨大输入分配。
'use strict'
function arrayLike(input) {
if (input === null || input === undefined) throw new TypeError('输入不能是 null 或 undefined')
const object = Object(input)
const number = +object.length
const length = Number.isNaN(number) || number <= 0
? 0
: Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER)
return { object, length }
}
function arrayMap(input, callback, thisArg) {
const { object, length } = arrayLike(input)
if (typeof callback !== 'function') throw new TypeError('callback 必须是函数')
const result = new Array(length)
for (let index = 0; index < length; index++) {
if (index in object) {
const value = Reflect.apply(callback, thisArg, [object[index], index, object])
Object.defineProperty(result, index, {
value, writable: true, enumerable: true, configurable: true
})
}
}
return result
}
function arrayReduce(input, callback, initialValue) {
const { object, length } = arrayLike(input)
if (typeof callback !== 'function') throw new TypeError('callback 必须是函数')
let index = 0
let accumulator
if (arguments.length >= 3) accumulator = initialValue
else {
while (index < length && !(index in object)) index++
if (index === length) throw new TypeError('空数组 reduce 必须提供初始值')
accumulator = object[index++]
}
for (; index < length; index++) {
if (index in object) accumulator = Reflect.apply(callback, undefined, [accumulator, object[index], index, object])
}
return accumulator
}
arrayMap([,2],x => x*2) 返回长度为 2、第一项仍为空位的数组;arrayMap([undefined],x => x) 则有一个真正存在且值为 undefined 的元素。arrayReduce([],fn,undefined) 返回 undefined,arrayReduce([],fn) 抛错,这正是不能用 initialValue !== undefined 判断初始值的原因。
循环使用快照 length,所以回调 append 到原长度以外的元素不会被访问;尚未访问的索引删除后会跳过,修改已存在但尚未读取的值会读取新值。这里用一元加号遵循数值转换行为,Symbol 或 BigInt 长度会抛错,而不是静默转成任意长度。面试实现应先讲契约和边界,再写代码,最后用空数组、稀疏数组、显式 undefined、继承索引和回调修改输入来验证。
示例与推演
手算 [,2,3] 的 reduce:不传初始值时先扫描到索引 1 的 2,累积只再处理 3,结果 5;初始值显式为 undefined 时必须进入回调规则,而不是当成缺省。map 输出长度保留,只有真正存在的索引被创建,不能简单 for…of 后 push。
易错点
手写核心流程不等于完整规范 polyfill。不要污染内置原型;Proxy、Array species 和极大数组等边界应明确支持范围。
参考回答可直接练习口述
下面提供独立的 arrayMap 和 arrayReduce,不覆盖 Array.prototype。它们展示普通数组及 array-like 对象的核心行为:开始时快照长度、跳过不存在的索引、访问继承索引、向回调传入原对象。不是完整规范 polyfill,不实现 Array 子类 species 等高级细节;map 结果仍受 JavaScript 数组长度上限约束,不应对任意巨大输入分配。