ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。 具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值
啥是尾调用
function outerFunction() {
return innerFunction(); // 尾调用
}
怎吗优化
在 没有ES6尾调用优化之前,执行这个例子会在内存中发生如下操作
- 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
- 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。
- 执行到 innerFunction 函数体,第二个栈帧被推到栈上。
- 执行 innerFunction 函数体,计算其返回值。
- 将返回值传回 outerFunction,然后 outerFunction 再返回值。
-
在 ES6尾调用优化之后,执行这个例子会在内存中发生如下操作
执行到 outerFunction 函数体,第一个栈帧被推到栈上。
- 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。
- 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction 的返回值。
- 弹出 outerFunction 的栈帧。
- 执行到 innerFunction 函数体,栈帧被推到栈上。
- 执行 innerFunction 函数体,计算其返回值。
将 innerFunction 的栈帧弹出栈外。
很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多 少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其 销毁,则引擎就会那么做。
尾调用优化条件
尾调用优化的条件就是确定外部栈帧真的没有必要存在了 。条件如下
- 代码在严格模式下执行
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
为何要在严格模式执行
之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用 f.arguments 和 f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此 尾调用优化要求必须在严格模式下有效,以防止引用这些属性。
不符合尾调用优化的例子
"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(condition) {
return condition ? innerFunctionA() : innerFunctionB();
}
尾调用与尾递归
差异化尾调用和递归尾调用是容易让人混淆的地方。无论是递归尾调用还是非递归尾调用,都可以 应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。不过,这个优化在递归场景下的效 果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧。