手写 map 与 reduce,如何保留空位并处理未提供初始值?

先理解原理和示例,再练习完整口述与追问。

固定初始长度;用 in 检查属性存在;回调签名正确;reduce 根据 arguments.length 判断初始值;不修改内置原型。

下面提供独立的 arrayMap 和 arrayReduce,不覆盖 Array.prototype。它们展示普通数组及 array-like 对象的核心行为:开始时快照长度、跳过不存在的索引、访问继承索引、向回调传入原对象。不是完整规范 polyfill,不实现 Array 子类 species 等高级细节;map 结果仍受 JavaScript 数组长度上限约束,不应对任意巨大输入分配。

js
'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 数组长度上限约束,不应对任意巨大输入分配。