ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。 具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值

啥是尾调用

  1. function outerFunction() {
  2. return innerFunction(); // 尾调用
  3. }

怎吗优化

在 没有ES6尾调用优化之前,执行这个例子会在内存中发生如下操作

  1. 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
  2. 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。
  3. 执行到 innerFunction 函数体,第二个栈帧被推到栈上。
  4. 执行 innerFunction 函数体,计算其返回值。
  5. 将返回值传回 outerFunction,然后 outerFunction 再返回值。
  6. 将栈帧弹出栈外。

    在 ES6尾调用优化之后,执行这个例子会在内存中发生如下操作

  7. 执行到 outerFunction 函数体,第一个栈帧被推到栈上。

  8. 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。
  9. 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction 的返回值。
  10. 弹出 outerFunction 的栈帧。
  11. 执行到 innerFunction 函数体,栈帧被推到栈上。
  12. 执行 innerFunction 函数体,计算其返回值。
  13. 将 innerFunction 的栈帧弹出栈外。

    很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多 少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其 销毁,则引擎就会那么做。

    尾调用优化条件

    尾调用优化的条件就是确定外部栈帧真的没有必要存在了 。条件如下

  • 代码在严格模式下执行
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回后不需要执行额外的逻辑
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包

    为何要在严格模式执行

    之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用 f.arguments 和 f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此 尾调用优化要求必须在严格模式下有效,以防止引用这些属性。

不符合尾调用优化的例子


    1. "use strict";
    2. // 无优化:尾调用没有返回
    3. function outerFunction() {
    4. innerFunction();
    5. }

    1. "use strict";
    2. // 无优化:尾调用没有直接返回
    3. function outerFunction() {
    4. let innerFunctionResult = innerFunction();
    5. return innerFunctionResult;
    6. }

    1. "use strict";
    2. // 无优化:尾调用返回后必须转型为字符串
    3. function outerFunction() {
    4. return innerFunction().toString();
    5. }

    1. "use strict";
    2. // 无优化:尾调用是一个闭包
    3. function outerFunction() {
    4. let foo = 'bar';
    5. function innerFunction() { return foo; }
    6. return innerFunction();
    7. }

    尾调用优化例子


    1. "use strict";
    2. // 有优化:栈帧销毁前执行参数计算
    3. function outerFunction(a, b) {
    4. return innerFunction(a + b);
    5. }

    1. "use strict";
    2. // 有优化:初始返回值不涉及栈帧
    3. function outerFunction(a, b) {
    4. if (a < b) {
    5. return a;
    6. }
    7. return innerFunction(a + b);
    8. }

    1. "use strict";
    2. // 有优化:两个内部函数都在尾部
    3. function outerFunction(condition) {
    4. return condition ? innerFunctionA() : innerFunctionB();
    5. }

    尾调用与尾递归

    差异化尾调用和递归尾调用是容易让人混淆的地方。无论是递归尾调用还是非递归尾调用,都可以 应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。不过,这个优化在递归场景下的效 果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧。