写给自己的 JavaScript 知识点拾遗 - 4

什么是递归

递归我们都知道,当一个程序调用自身,那么就可以被认为是一个递归。最简单的递归就是阶乘:

1
2
3
4
5
function factorial(n) {
if (n == 1) return n;
return n * factorial(n - 1)
}
console.log(factorial(5))

递归需要一个出口

没有出口的递归,回让程度无法结束倒是,轻则死锁,重则崩溃。所以递归都是需要一个边界条件的,例如上边阶乘中的 n == 1 就是一个边界条件。

性能优化

递归是一个很耗费资源的操作,可以而且需要被优化。尾调用就是优化递归的方法。它是指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。

🌰栗子:

1
2
3
function f(x){
return g(x);
}

尾调用优化的很多细节下次再记录,这里说一下为什么尾调用能够对递归进行优化。

这里要涉及到 javaScript 的执行上下文栈。每个函数执行的时候,都会创建一个执行上下文,或者称为调用帧,压入执行上下文栈中。当函数实行完毕,该函数的执行上下文从栈中弹出,然后继续执行接下来的逻辑。那么我门上面给出的尾调用的例子是这样的:

我们定义一个执行上下文栈:

1
contextStack = [];

那么它的执行上下文栈的变化是:

1
2
3
4
contextStack.push(<f> context);
contextStack.pop();
contextStack.push(<g> context);
contextStack.pop();

很清楚,执行完毕马上其上下文就被弹出销毁。最为对比,我们看一个非尾调用的例子:

1
2
3
function f(x){
return g(x)+1;
}

那么它的执行上下文栈的变化是:

1
2
3
4
contextStack.push(<f> context);
contextStack.push(<g> context);
contextStack.pop();
contextStack.pop();

发生这样的变化的原因是 f 函数中的常量 1 必须保持在执行上下文中等待 g 函数调用返回进行计算后,才能回收,f 函数的执行上下文才能出栈。想想一下,如果递归的层级很深,那么一定是会爆栈的。资源耗费太大了。

阶乘优化

下面我来按照尾调用的思想,优化一下阶乘函数。

1
2
3
4
5
6
function factorial(n, res) {
if (n == 1) return res;
return factorial(n - 1, n * res)
}

console.log(factorial(4, 1))

很粗鲁,但是它确实是一个尾递归了。

最后让我困惑的是下面这个函数是不是尾调用:

1
2
3
function f(x){
g(x);
}

显然不是的,因为如果你没有制定返回值,函数默认返回的是 undefined:

1
2
3
4
function f(x){
g(x);
return undefined;
}