怎样避免深递归导致栈溢出?实现蹦床函数、大数阶乘和迭代版数组扁平化。

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

规范中的尾调用优化只有 Safari 实现,不能依赖。可行办法:改写为循环;用蹦床函数让递归返回“下一步”的函数,由外层循环执行;用显式栈模拟递归。阶乘结果很快超出 Number.MAX_SAFE_INTEGER,需要 BigInt。扁平化用栈迭代,跳过数组空位并检测循环引用。

每次函数调用都占用一个栈帧,递归层数过深(通常几千到几万层,取决于引擎与帧大小)会抛出 RangeError。蹦床函数让递归函数不直接调用自己,而是返回一个 thunk(无参函数),外层 while 循环反复执行 thunk,每次调用都在同一层栈上完成。为了区分“返回的是下一步”还是“结果本身就是函数”,给 thunk 打上 Symbol 标记。

迭代扁平化时,栈中保存“当前处理的数组和下标”,遇到子数组就压栈,处理完弹出,顺序与递归一致。空位([1, , 2])用 in 判断跳过,与 Array.prototype.flat 行为一致。同一个子数组在不同位置出现是合法的共享引用,只有当它出现在自己的处理路径上才是循环,所以用“正在处理”的集合检测,处理完后移除。

js
const THUNK = Symbol('thunk')
const thunk = fn => Object.assign(fn, { [THUNK]: true })

function trampoline(fn) {
  return function (...args) {
    let result = fn.apply(this, args)
    while (typeof result === 'function' && result[THUNK]) result = result()
    return result
  }
}

const sumTo = trampoline(function sum(n, acc = 0) {
  return n === 0 ? acc : thunk(() => sum(n - 1, acc + n))
})

function factorial(n) {
  if (!Number.isInteger(n) || n < 0) throw new RangeError('n 必须是非负整数')
  let result = 1n
  for (let i = 2n; i <= BigInt(n); i++) result *= i
  return result
}

function flattenDeep(input) {
  const result = []
  const active = new Set([input])
  const stack = [{ array: input, index: 0 }]
  while (stack.length) {
    const frame = stack[stack.length - 1]
    if (frame.index >= frame.array.length) {
      active.delete(frame.array)
      stack.pop()
      continue
    }
    const index = frame.index++
    if (!(index in frame.array)) continue
    const item = frame.array[index]
    if (Array.isArray(item)) {
      if (active.has(item)) throw new TypeError('数组存在循环引用')
      active.add(item)
      stack.push({ array: item, index: 0 })
    } else {
      result.push(item)
    }
  }
  return result
}

推演:sumTo(100000) 返回 5000050000,直接递归版本在多数引擎会栈溢出;factorial(25) 返回 15511210043330985984000000n,用 Number 计算会丢失精度;flattenDeep([1, [2, [3, , 4]], 5]) 返回 [1, 2, 3, 4, 5]。

BigInt 不能与 Number 混合运算,1n + 1 会抛 TypeError;JSON.stringify 不支持 BigInt。扁平化用 toString().split(',') 会把所有元素变成字符串。

浏览器基本不支持尾调用优化,所以深递归要么改成循环,要么用蹦床函数:递归函数返回带标记的 thunk,外层 while 循环反复执行,栈深度始终是常数;还可以用显式栈模拟递归,比如迭代版扁平化,栈里存数组和下标,跳过空位,用正在处理的集合检测循环引用。阶乘很快超出安全整数,用 BigInt 计算,注意 BigInt 不能和 Number 混算。