← 返回题目列表

如何手写数组的 map、filter 和 reduce?

高频 中等 第 12 / 27 题 更新于 2026/07/29
手写代码ArrayPolyfillJavaScript

简化版

map 返回等长新数组,filter 返回满足条件的新数组,reduce 把数组折叠成一个累计值。手写时要注意跳过稀疏数组空洞、回调参数顺序、thisArg、初始值缺省以及空数组调用 reduce 的错误边界。

详细版

基础实现要遵循回调签名:callback(value, index, array)map/filter 可从左到右遍历并判断 i in arrreduce 若没有传初始值,需要找第一个存在的元素作为初始累计值。

Array.prototype.myMap = function (callback, thisArg) {
  const arr = Object(this)
  const len = arr.length >>> 0
  const result = new Array(len)
  for (let i = 0; i < len; i += 1) {
    if (i in arr) {
      result[i] = callback.call(thisArg, arr[i], i, arr)
    }
  }
  return result
}

面试官常追问稀疏数组,比如 [1, , 3].map(...) 中间空洞不会调用回调。教学版可以覆盖核心语义,不必完整实现 ArraySpeciesCreate 等规范细节。

完整版教学

一、三者的语义差异

方法输入输出关系是否改变原数组典型用途
mapn 个元素映射成 n 个位置不改变数据转换
filtern 个元素筛出 0 到 n 个元素不改变条件筛选
reducen 个元素折叠成 1 个累计值不改变汇总、分组、链式计算

map 关注“一一对应”,filter 关注“是否保留”,reduce 关注“累计状态如何变化”。三者都不是简单 for 循环替代品,而是把不同的数据处理意图固化成 API。

记忆钩子:map 看返回值,filter 看真假,reduce 看累计器。

二、手写 map

Array.prototype.myMap = function (callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function')
  }

  const arr = Object(this)
  const len = arr.length >>> 0
  const result = new Array(len)

  for (let i = 0; i < len; i += 1) {
    if (i in arr) {
      result[i] = callback.call(thisArg, arr[i], i, arr)
    }
  }

  return result
}

Object(this) 让类数组对象也能借用方法。length >>> 0 把 length 转成非负 32 位整数,是很多数组 polyfill 的常见写法。

三、手写 filter

Array.prototype.myFilter = function (callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function')
  }

  const arr = Object(this)
  const len = arr.length >>> 0
  const result = []

  for (let i = 0; i < len; i += 1) {
    if (i in arr) {
      const value = arr[i]
      if (callback.call(thisArg, value, i, arr)) result.push(value)
    }
  }

  return result
}

filter 返回的是原元素本身,不是回调返回值。回调只负责给出是否保留的判断,任何 truthy 值都表示保留。

四、手写 reduce

Array.prototype.myReduce = function (callback, initialValue) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function')
  }

  const arr = Object(this)
  const len = arr.length >>> 0
  let index = 0
  let accumulator

  if (arguments.length >= 2) {
    accumulator = initialValue
  } else {
    while (index < len && !(index in arr)) index += 1
    if (index >= len) throw new TypeError('Reduce of empty array with no initial value')
    accumulator = arr[index]
    index += 1
  }

  for (; index < len; index += 1) {
    if (index in arr) {
      accumulator = callback(accumulator, arr[index], index, arr)
    }
  }

  return accumulator
}

reduce 最容易错在初始值。[].reduce(fn) 应该抛错,[].reduce(fn, 0) 应该返回 0;[,,3].reduce(fn) 会把 3 当作初始累计值,回调一次都不执行。

五、稀疏数组为什么要用 i in arr

const arr = [1, , 3]
let count = 0
arr.map(() => count += 1)
console.log(count) // 2

空洞不是值为 undefined 的元素,而是这个下标不存在。i in arr 能区分空洞和显式的 undefined,这也是手写数组方法时最常见的细节题。

[1, , undefined]
 index 0: 存在,值 1
 index 1: 不存在,空洞
 index 2: 存在,值 undefined

六、复杂度与副作用边界

三者时间复杂度通常都是 O(n)map 会分配等长结果数组,空间复杂度 O(n)filter 最多也可能保留全部元素;reduce 的额外空间取决于累计值。

回调可以修改原数组,这会带来难读的边界。简化版教学通常固定初始长度,再按下标访问,避免新增元素无限参与遍历;生产环境更应该避免在这些回调里修改正在遍历的数组。

七、常见误区与追问

  • 误区:map 可以用来过滤元素。 map 返回等长结构,过滤应使用 filter
  • 误区:filter 返回回调的返回值数组。 它返回原数组中通过测试的元素。
  • 误区:reduce 没有初始值也总能运行。 空数组或全空洞数组会抛错。
  • 追问:为什么要跳过空洞? 原生数组迭代方法不会对不存在的下标调用回调。
  • 追问:thisArg 有什么用? 它指定非箭头回调执行时的 this
  • 追问:类数组能借用吗? 可以,通过 Object(this) 和 length 读取支持主要场景。

八、加强记忆

数组三件套可以按“变、筛、归”记:map 负责把每个位置变成新值,filter 负责筛出留下的元素,reduce 负责把一串元素归并成一个累计结果。手写时抓住回调参数、返回结构、稀疏数组和初始值边界,就能从普通 for 循环写到接近原生语义。