尾调用优化
今天在面试时遇到这样一个笔试题目,如何优化以下一段代码,实现尾调用优化?
function factorial(n) { if (n <= 1) { return 1; return n * factorial(n - 1); }
答案:
function factorial(n, p = 1) { if (n <= 1) { return 1 * p; } else { let result = n * p; // 被优化 return factorial(n - 1, result); } }
之前在《红宝书》以及《深入理解ES6》看到过尾调用的这部分内容,但确实忘记如何修改这段代码了,只记得要在最后只返回一个函数,而不能进行任何的额外操作。所以此题并没有完全回答正确。
回来也是好好复习了以下这部分的内容,以备下次遇到这类型不知道如何回答。
首先尾调用优化是在ES6才有的引擎优化,它改变了尾部调用的系统。在调用函数的结尾是另一个函数的最后语句。如下:
function doSomething() { return doSomethingElse(); // 尾调用 }
在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧( stack frame)被创建并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。
function outerFunction() { return innerFunction(); // 尾调用 }
在 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 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
ES6 在严格模式下会为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧:
注意 之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用 f.arguments 和 f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此 尾调用优化要求必须在严格模式下有效,以防止引用这些属性。
1. 尾调用不能引用当前栈帧中的变量(意味着该函数不能是闭包);
2. 进行尾调用的函数在尾调用返回结果后不能做额外操作;
3. 尾调用的结果作为当前函数的返回值。
正确示例:
"use strict"; function doSomething() { // 被优化 return doSomethingElse(); }
"use strict"; // 有优化:栈帧销毁前执行参数计算 function outerFunction(a, b) { return innerFunction(a + b); } // 有优化:初始返回值不涉及栈帧 function outerFunction(a, b) { if (a < b) { return a; } return innerFunction(a + b); } // 有优化:两个内部函数都在尾部 function outerFunction(condition) { return condition ? innerFunctionA() : innerFunctionB(); }
错误示例:
"use strict"; function doSomething() { // 未被优化:缺少 return doSomethingElse(); }
"use strict"; function doSomething() { // 未被优化:在返回之后还要执行加法 return 1 + doSomethingElse(); }
"use strict"; function doSomething() { // 未被优化:调用并不在尾部 var result = doSomethingElse(); return result; }
"use strict"; function doSomething() { var num = 1, func = () => num; // 未被优化:此函数是闭包 return func(); }
尾调用优化允许某些函数的调用被优化,以保持更小的调用栈、使用更少的内存,并防止堆栈溢出。当能进行安全优化时,它会由引擎自动应用。不过你可以考虑重写递归函数,以便能够利用这种优化。