尾调用和尾递归优化
尾调用
参考链接
TL; DR
尾调用就是指在某个函数的最后一步调用另一个函数。亦或者说把函数调用放在函数的最后。
function f(x) {
return g(x);
}
调用栈
当在函数中调用另一个新的函数时,将会在运行栈中新建一个栈帧,并把新的函数执行所需要的变量,寄存器等放入这个栈帧。上一个函数所对应的栈帧被称为调用帧。如下图所示:
如之前所说的,尾调用就是一个函数执行的最后一步是将另一个函数调用并返回。这样就可以不用保存调用者栈帧中的参数,变量等,甚至可以不用保存调用帧,可以大大减少调用栈的大小。
尾调用优化
TL; DR
尽力做到每次调用函数时,只保留内层被调用函数的栈帧,这样可以大大节省内存。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
尾递归
函数尾调用自身,就叫做尾递归
递归虽好,但是非常耗内存。如果能使用尾调用优化的思想去优化递归,那将是极好的。接下来我们看一下尾递归和非尾递归的性能差异。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
// 尾递归优化
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
function test1(n) {
console.time('test');
console.log(Fibonacci(n));
console.timeEnd('test')
}
function test2(n) {
console.time('test');
console.log(Fibonacci2(n));
console.timeEnd('test')
}
test1(10) // test: 0.873046875ms
test2(10) // test: 0.2119140625ms
test1(40) // test: 1514.0390625ms
test2(40) // test: 0.31689453125ms
test2(1000) // test: 0.67919921875ms
从上面的结果中我们可以很明显的看出尾递归优化的作用。
想要把普通递归函数改写成尾递归,就要把所有的内部变量改写成函数的参数。
为了让函数保持原来参数的形式,可以使用es6的函数默认值来初始化应该为内部变量的参数。
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
因为正常模式下,函数内部的两个变量可以跟踪函数的调用栈。
- func.arguments: 返回调用时函数的参数
- func.caller: 返回调用当前函数的那个函数
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效;