尾调用优化

来源:互联网 发布:玲珑密保网络错误 编辑:程序博客网 时间:2024/05/21 17:27

什么是尾调用?

尾调用是函数式编程的重要概念之一,即在某个函数的最后一步来调用另一个函数。

function f(x){    return g(x);}

而除此之外的其他写法和形式都不是尾调用。比如说:

// 情况一function f(x){    let y = g(x);    return y;}// 情况二function f(x){    return g(x) + 1;}// 情况三function f(x){    g(x);}

尾调用之所以被称为尾调用,就是因为它的调用位置比较特殊。

还记得我们初学js函数部分吧,函数名保存在栈内存,函数值保存在堆内存中。函数调用会产生一个‘调用记录’,用来保存调用位置和内部变量等信息。也就是说,在函数A内部调用函数B,那么在函数A的调用帧上方,还会形成一个B的调用帧。等B运行结束,将结果返回给了A,B的调用帧才消失。
当然了,如果函数B内部调用了函数C,那么B的上方也会有函数C的调用帧。以此类推,从而形成了‘调用栈’(call stack)。

但是当嵌套调用比较多的时候,比如说递归的时候,会造成相当多的调用帧。
所以我们需要用到尾调用优化,来减少调用帧的使用。尾调用因为在函数的最后一步,所以不需要保留外层函数的调用帧。

例子:

function f(){    let m = 1;    let n = 2;    return g(m + n);}f();// 等同于function f(){    return g(3);}// 等同于g(3);

在上面的代码中,如果函数 g 不是尾调用的话,那么函数 f 就不能结束,而应该去保存内部变量m和n的值以及 g 的调用位置等信息。
但正因为函数 g 是尾调用,所以函数 f 就结束了,所以当函数执行到函数 g 的时候,就可以删除函数 f(x) 的调用帧,只保留 g(3) 的调用帧。

这就是尾调用优化。 即只保留内层函数的调用帧。如果所有函数都是尾调用,那么就能实现每次执行都是只有一项调用帧,这将大大节省内存。

尾递归

尾递归其实也是尾调用的一种。函数调用自身,称为递归。
递归是非常消耗内存的,因为需要保存N个调用帧,所以说特别容易发生‘栈溢出’错误。但是尾递归可以完美解决。

function factorial(n){    if(n === 1) return 1;    return n * factorial(n - 1);}factorial(5)    // 120

这是一个计算5的阶乘,所以需要保存5个调用栈。
对了。突然想起来的直接说吧。
为了减少函数的耦合,我们应该使用arguments.callee

而如果改为尾递归的话,就只需要保留一个调用记录了。

function factorial(n, total = 1){    if(n === 1) return total;    return arguments.callee(n - 1, n * total);}factorial(5)   // 120

这样就可以实现尾递归优化,我们这里使用ES6的函数默认值写法来实现多参变一参,当然也可以使用 柯里化。

另外一个例子就是计算 Fibonacci 数列, 也能充分说明尾递归优化的重要性。

function Fibonacci (n) {    if (n <= 1) return 1;    return Fibonacci(n - 1) + Fibonacci(n - 2);}Fibonacci(10);   // 89Fibonacci(100);  // 堆栈溢出Fibonacci(500);  // 堆栈溢出

而经过尾递归优化的 Fibonacci 数列实现如下:

function Fibonacci(n, ac1 = 1, ac2 = 1){    if(n <= 1) return ac2;    return Fibonacci(n - 1, ac2, ac1 + ac2)}Fibonacci(100);     // 573147844013817200000Fibonacci(1000);    // 7.0330367711422765e+208

所以,‘尾调用优化’对于递归操作意义重大。在ES6中只要使用尾递归,就不会发生栈溢出,就会节省内存。