1.无尾调用优化的
console.time('执行时间'); function fibonacci(n) { return Number(fibonacciTool(BigInt(n))); } function fibonacciTool(n) { if (n === 0n) { return 0n; } if (n === 1n) { return 1n; } return fibonacci(n - 1n) + fibonacci(n - 2n); } console.log(fibonacci(40)); console.timeEnd('执行时间');执行以上代码,得到结果如下截图 2.有尾调用优化的
"use strict" console.time('执行时间'); function fib(n) { const result = fibTool(0n, 1n, BigInt(n)); return Number(result); }; function fibTool(a, b, n) { if (n === 0n) { return a; } return fibTool(b, a + b, n - 1n); } console.log(fib(40)); console.timeEnd('执行时间');可以看到,没有尾调用优化的代码的执行时间为18秒左右,有尾调用优化的代码的时间仅为4.6毫秒左右,可见,尾调用优化对于程序的性能影响是非常大的,尤其是在多层递归调用的情况下,那么下面看看什么叫做尾调用优化。
在ES6优化之前,执行这个例子会在内存中发生如下操作:
(1)执行到outerFunction函数时,第一个栈帧被推到栈上。
(2)执行到outerFunction函数体,到return语句。计算返回值必须先计算innerFunction。
(3)执行到innerFunction函数体,第二个栈帧被推倒栈上。
(4)执行innerFunction函数体,计算其返回值。
(5)将返回值传回outerFunction,然后outerFunction再返回值。
(6)将栈帧弹出栈外。
在ES6优化之后,执行这个例子会在内存中发生如下操作。
(1)执行到outerFunction函数时,第一个栈帧被推到栈上。
(2)执行到outerFunction函数体,到return语句。为求值返回语句,必须先求值innerFunction。
(3)引擎发现把第一个栈帧弹出栈外也没问题,因为innerFunction的返回值也是outerFunction的返回值。
(4)弹出outerFunction的栈帧。
(5)执行到innerFunction函数体,栈帧被推倒栈上。
(6)执行innerFunction函数体,计算返回值。
(7)将innerFunction的栈帧弹出栈外。
很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多少次嵌套函数,都只有一个栈帧。这就是ES6尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
尾调用优化的条件就是确定外部栈帧真的没有必要存在了。设计的条件如下:
(1)代码在严格模式下执行;
(2)外部函数的返回值是对尾调用函数的调用;
(3)尾调用函数返回后不需要执行额外的逻辑;
(4)尾调用函数不是引用外部函数作用域中自由变量的闭包。
下面展示了几个违反上述条件的函数,因此都不符合尾调用优化的要求:
//无优化:尾调用没有返回 "use strict"; function outerFunction(){ innerFunction(); } //无优化:尾调用没有直接返回 "use strict"; function outerFunction(){ let innerFunctionResult = innerFunction(); return innerFunctionResult; } //无优化:尾调用返回后进行了额外的操作 "use strict"; function outerFunction(){ return innerFunction().toString(); } //无优化:尾调用是一个闭包 "use strict"; function outerFunction(){ let foo = 'bar'; function innerFunction(){return foo;} return innerFunction(); }下面是几个符合尾调用优化条件的例子:
//有优化:栈帧销毁前执行参数计算 "use strict"; function outerFunction(a, b){ return innerFunction(a + b); } //有优化:初始返回值不涉及栈帧 "use strict"; function outerFunction(a, b){ if (a < b){ return a; } return innerFunction(a + b); } //有优化:两个内部函数都在尾部 "use strict"; function outerFunction(a, b){ return condition ? innerFunctionA() : innerFunctionB(); }差异化尾调用和递归尾调用是最容易让人混淆的地方。无论是递归尾调用还是非递归尾调用,都可以应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。不过,这个优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧,比如说在本文开头举的fibonacci递归调用的例子。
注意:之所以要求严格模式,主要是因为在非严格模式下函数调用中允许使用f.arguments和f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性。
参考资料:《JavaScript高级程序设计(第四版)》