我们知道,递归非常耗费内存,因为需要同时保存多个调用帧,很容易发生“栈溢出”错误。
例如,计算阶乘的函数:
function factorial(n) {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
}
// output:
// factorial(5) - 120
// factorial(10000) - "RangeError: Maximum call stack size exceeded"
当我们的n大到一定程度时,就会产生栈溢出的错误。因此,需要对其进行优化,降低栈帧开销。
尾递归(Tail Recursion)
在了解尾递归之前,我们先来了解什么叫做尾调用。它是指某个函数的最后一步是调用另外一个函数,例如:
function f(a) {
return a > 0 ? g1(a) : g2(a);
}
下述例子不属于尾调用:
function f(a) {
g(a);
}
现在,我们回过头来了解什么是尾递归?
所谓的尾递归,就是尾调用自身。即递归调用是函数最后执行的操作,并且函数的返回值直接来自递归调用的结果,而不需要保留外层函数的调用帧。由于这种特性,尾递归可以被编译器或解释器优化为迭代形式,从而避免栈溢出问题,提高执行效率。这种优化也称为“尾调用优化”(Tail Call Optimization)。
使用尾递归形式实现factorial函数:
function factorial(n, acc) {
if (n === 1) {
return acc;
}
return factorial(n - 1, n * acc)
}
*在Node.js 中处理递归问题
Node不支持尾调用优化(V8不支持),我们可以使用:
- 循环的方式避免栈溢出问题,如:
function factorial(n) {
let acc = 1;
while (n > 1) {
acc *= n;
n--;
}
return acc;
}
- 使用process.nextTick()拆解成多个微任务,如:
function factorial(n, cb) {
let total = 1;
function calc() {
if (n === 1) {
return cb(total);
}
total *= n--;
process.nextTick(calc);
}
calc();
}