尾调用优化

235 阅读3分钟

尾调用优化

ECMAScript 6 新增的一项内存管理优化机制, 让 JavaScript 引擎在满足条件时可以重用栈帧 非常适合于尾调用

尾调用的概念

什么是尾调用? 外部函数的返回值是内部函数的返回值, 比如:

function outerFunction () {
  return innerFunction (); // 尾调用
}

使用尾调用优化和不适用尾调用优化的区别?

以上面代码块里面的代码为例:

未优化:

  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 的栈帧弹出栈外

尾调用满足的条件

一句话阐述

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

分条件展示

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

bad case

function outerFunction () {
  innerFunction();
}

尾调用没有返回, 所以不属于尾调用函数

function outerFunction () {
  let innerFunctionResult = innerFunction();
  return innerFunctionResult;
}

尾调用没有直接返回 必须要直接返回才属于尾调用函数

function outerFunction () {
  return innerFunction().toString();
}

尾调用没有直接返回, 返回后还必须转为字符串. 必须要直接返回 再没有别的操作才符合尾调用函数

function outerFunction() {
  let foo = 'bar';
  function innerFunction(){
    return foo;
  } 
  return innerFunction();
}

尾调用是一个闭包, 不符合尾调用函数的规定

good case

function outerFunction(a, b) {
  return innerFunction(a + b);
}

符合, 栈帧销毁前执行参数计算

function outerFunction(a, b) {
  if (a < b) {
    return a;
  }
  return innerFunction(a + b);
}

符合, 初始的返回值不设计栈帧

function outerFunction() {
  return condition ? innerFunctionA() : innerFunctionB();
}
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

符合, 尾调用不一定是出现在函数的尾部, 只要是最后一步操作即可

差异化尾调用和递归尾调用

引擎并不区分尾调用的是自身函数还是其他函数

不过很明显, 递归场景下的优化效果最明显, 递归最容易在栈内存中迅速产生大量栈帧.

递归案例

fib 函数, 斐波那契数列

// 经典递归
function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n -1) + fib(n - 2)
}

现在这个明显不符合尾调用优化的条件 如何修改?

function fib(n){
  return fibImpl(0, 1, n)
}

function fibImpl(a, b, n) {
  if (n === 0) {
    return a;
  }
  return fibImpl(b, a + b, n - 1)
}