尾调用优化
什么是尾调用
尾调用(Tail Call)是函数式变成的重要概念,本身很简单,就是指函数的最后一步,调用另一个函数。
function f(x){
return g(x)
}
// 函数最后一步调用了 g(x)
复制代码
以下几种都不属于尾调用
//1
function f(x){
let y = g(x);
return y;
}
//2
function f(x){
return g(x) + 1
}
//3
function f(x){
g(x);
}
复制代码
1 是函数g之后还有赋值操作
2 是 调用后还有操作
3 相当于
function f(x){
g(x);
return undefined;
}
复制代码
尾调用不一定出现在函数的尾部
function f(x) {
if(x > 0){
return m(x);
}
return n(x);
}
复制代码
m 和 n 都属于尾调用,都是 f 的最后一步
尾调用优化
尾调用,特殊就在于他的调用位置。
函数调用,会在内存形成一个 调用记录 ,也叫 调用帧,保存调用位置和内部变量等信息。
如果函数A内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧。等B运行结束,将结果返回A,B的调用帧才会消失。如果函数B内部还调用函数C,那么还有一个C的调用帧,以此类推,调用帧就形成一个“调用栈(call stack)”
尾调用是函数的最后一步操作,所以不需要保存外部函数的调用帧,因为调用位置和内部变量等都不会再用到了,直接用内层函数的调用帧取代外层函数即可。
function f(){
let m = 1;
let n = 2;
return g(m+n);
}
f();
// 等同
function f() {
return g(3)
}
f()
//等同于
g(3)
复制代码
分析:如果上边函数g不是尾调用的话,f就需要保存内部变量 m 和 n,g的调用位置信息等。但是调用 g 之后,函数 f 就结束了,就可以删除 f 的调用帧,只保留 g(3)的调用帧。这将大大节省内存。
只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就不能进行“尾调用优化”
尾递归
函数调用自身称为递归,如果尾调用自身就称为尾递归。
递归非常消耗内存,要同时保存成百上千的调用帧,很容易栈溢出,但是对于尾递归,则永远不会发生 栈溢出 错误。
function fac(n){
if(n === 1){
return 1
}
return n * fac(n-1)
}
fac(5)
复制代码
这个阶乘函数的时间复杂度为 O(n)
改成尾递归
function fac(n, total){
if(n === 1) return total;
return fac(n - 1,n * total);
}
fac(5,1)
复制代码
斐波那契数列,也是一个递归的例子
function fibnacci(n){
if(n <= 1) return 1;
return fibnacci(n-1) + fibnacci(n-2);
}
fibnacci(10) //89
fibnacci(100) //堆栈溢出
复制代码
尾调用递归优化
function fibnacci(n,ac1,ac2){
if(n <= 1) return 1;
return fibnacci(n,ac2,ac1+ac2)
//都不会溢出
//fibnacci(10) 89
//fibnacci(100)
//fibnacci(1000)
//fibnacci(10000) infinity
复制代码
递归函数的改写
尾递归往往需要改写递归函数,确保最后一步只调用自身,做到这一点的方法就是把所有用到的内部变量改成函数的参数,不过也有一些缺点,就是看起来不是很直观。
不直观的问题也是可以解决的。
function tailFac(n, total){
if(n === 1) retturn total;
return tailFac(n - 1, n * total)
}
function fac(n){
return tailFac(n, 1)
}
复制代码
函数式编程还有一个概念,叫做“柯里化”,意思是,将多参数转化为单参数形式。这里也可以使用柯里化
function currying(){
return function (m){
return fn.call(this, m, n)
};
}
function tailFactorial(n, total){
if (n === 1) return total;
return tailFactorial(n-1, n * total)
}
const factorial = currying(tailFactorial, 1);
factorial(5)
复制代码
使用ES6的函数默认值
function factorial(n, total = 1){
if(n === 1) return 1;
return factorial(n - 1, n * total)
}
factorial(5)
复制代码