怎样避免深递归导致栈溢出?实现蹦床函数、大数阶乘和迭代版数组扁平化。
先理解原理和示例,再练习完整口述与追问。
回答要点
规范中的尾调用优化只有 Safari 实现,不能依赖。可行办法:改写为循环;用蹦床函数让递归返回“下一步”的函数,由外层循环执行;用显式栈模拟递归。阶乘结果很快超出 Number.MAX_SAFE_INTEGER,需要 BigInt。扁平化用栈迭代,跳过数组空位并检测循环引用。
原理拆解
每次函数调用都占用一个栈帧,递归层数过深(通常几千到几万层,取决于引擎与帧大小)会抛出 RangeError。蹦床函数让递归函数不直接调用自己,而是返回一个 thunk(无参函数),外层 while 循环反复执行 thunk,每次调用都在同一层栈上完成。为了区分“返回的是下一步”还是“结果本身就是函数”,给 thunk 打上 Symbol 标记。
迭代扁平化时,栈中保存“当前处理的数组和下标”,遇到子数组就压栈,处理完弹出,顺序与递归一致。空位([1, , 2])用 in 判断跳过,与 Array.prototype.flat 行为一致。同一个子数组在不同位置出现是合法的共享引用,只有当它出现在自己的处理路径上才是循环,所以用“正在处理”的集合检测,处理完后移除。
示例与推演
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 混算。