JS递归优化

136 阅读1分钟

我们知道,递归非常耗费内存,因为需要同时保存多个调用帧,很容易发生“栈溢出”错误。

例如,计算阶乘的函数:

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不支持),我们可以使用:

  1. 循环的方式避免栈溢出问题,如:
function factorial(n) {
    let acc = 1;

    while (n > 1) {
        acc *= n;
        n--;
    }
  
    return acc;
}
  1. 使用process.nextTick()拆解成多个微任务,如:
function factorial(n, cb) {
    let total = 1;

    function calc() {
        if (n === 1) {
            return cb(total);
        }

        total *= n--;

        process.nextTick(calc);
    }

    calc();
}