1.
(1)尾调用:指某个函数的最后一步是调用另一个函数。
例如:
function a(n){
return b(n);
}
(最后一步调用并不意味着在函数的尾部,只要是最后一步即可)
function a(n){
if(n>1){
return b(n);
}
return c(n);
}
(2)什么样的情况不是尾调用:
情况一:
function f(x){
let y = g(x);
return y;
}
解释:调用g之后,还有赋值操作,故不属于尾调用。
情况二:
function f(x){
return g(x) + 1;
}
解释:也是在调用g之后,还有后续别的操作,故也不属于尾调用。
情况三:
function f(x){
g(x);
}
解释:情况三相当于:
function f(x){
g(x);
return undefined;
}
(3)尾调用的优化:(只保留内存函数的调用帧)
《1》:尾调用不同于其他调用的点在于他特殊的调用位置。
《2》:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call
stack)
《3》:尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了.
《4》:如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
《5》只有不再用到外层函数的内部变量时,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行尾调用优化。
function foo(a){
var one="I'm the first";
function add(b){
return b+one;
};
return add(a);
};
备注:上述代码不会进行尾调用优化,因为内层函数用到了外层函数的内部变量。
2.
(1)尾递归:函数调用自身称为递归,如果尾调用自身,则是尾递归。
function tail(n) {
if (n === 1) return 1;
return n * tail(n - 1);
}
tail(3);
上述代码的时间复杂度为o(n),最多需要保存n个调用记录;
function tail(n,total){
if(n===1){
return total;
}
return tail(n-1,n*total);
}
上述代码的时间复杂度为o(1),只保留一个调用记录.
(2)递归函数的改写:
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
备注:参考资料(ES6官方教程)