JS—尾调用优化

1.尾调用(Tail Call)

尾调用是函数式编程的一个重要概念,本身非常检点。就是:某个函数的最后一步是调用另一个函数

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

 函数f的最后一步是调用函数g,这就叫尾调用。

以下情况,都不属于尾调用:

 1     // 情况一
 2     function f(x) {
 3         let y = g(x)
 4         return y
 5     }
 6 
 7     // 情况二
 8     function f(x) {
 9         return g(x) + 1
10     }
11 
12     // 情况三
13     function f(x) {
14         g(x)
15     }

上面代码中:

  • 情况一:调用函数g之后,还有赋值操作,所以不属于尾调用
  • 情况二:调用后还有操作
  • 情况三等同于:
    • 1 function f(x) {
      2   if (x > 0) {
      3     return m(x)
      4   }
      5   return n(x);
      6 }

尾调用不一定出现在函数尾部,只要是最后一步操作即可:

1 function f(x) {
2   if (x > 0) {
3     return m(x)
4   }
5   return n(x);
6 }

 

上面代码中:函数m和n都属于尾调用,因为它们都是函数f的最后一步操作

2.尾调用优化

尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

(1)正常函数调用:

  函数调用会在内存形成一个“调用记录”,又称“调用帧(call frame)”,保存调用位置和内部变量等信息,调用帧可以理解为栈中的一格,如下所示:

   JS—尾调用优化

  如一个函数:

  • function a(){
      b()
    }
  • 上面函数a执行的过程如下:
    • 执行到a()时,a被推入调用帧底部
    • 执行到a()中的b()方法时,b被压入a上方
    • b()执行结束,b()从调用栈中移除
    • a()执行结束,a()从调用栈移除,调用栈清空
    • 以此类推,多个执行语句先后入栈出栈

(2)函数尾调用

  尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

 1 function f(){
 2     let n = 1
 3     let m = 2
 4     return g(n + m)
 5 }
 6 f()
 7 
 8 // 等同于
 9 function f(){
10     return g(3)
11 }
12 f()
13 
14 // 等同于
15 g(3) 

上面代码,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值,g的调用位置等信息。但是由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。这就叫尾调用优化(Tail call optimization),即只保留内层函数的调用帧,如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化“的意义。

注意:只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行”尾调用优化“

1 function addOne(a){
2   var one = 1;
3   function inner(b){
4     return b + one;
5   }
6   return inner(a);
7 }

 

因为内层函数inner用到了外层函数addOne的内部变量one。

注意:目前自会有Safari浏览器支持尾调用优化,Chrome和Fiefox都不支持

3.尾递归

函数调用自身,称为递归,如果尾调用自身,就称为尾递归

 

 

 

 

 

 

JS—尾调用优化

上一篇:图片预览引发的思考 zoomerang.js 解析


下一篇:一 html基础