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)”,保存调用位置和内部变量等信息,调用帧可以理解为栈中的一格,如下所示:
如一个函数:
-
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.尾递归
函数调用自身,称为递归,如果尾调用自身,就称为尾递归