ES6中的TCP尾调用优化问题

ES6中的TCP尾调用优化问题

一、什么是尾调用优化 TCP (Tail Call Optimization)

  先来明白什么是尾调用:即外部函数的返回值是一个内部函数的返回值。比如:

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

  ECMAScript6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。这项优化非常适合“尾调用”。

  在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 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。

二、尾调用优化的条件

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

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

  下面列举违反上述条件的调用情况:

"use strict";
// 无优化:尾调用没有返回 
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();
}

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

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

三、尾调用优化的代码

  通过把简单的递归函数转换为待优化的代码来加深对尾调用优化的理解。下面是一个通过递归计算斐波纳契数列的函数:

function fib(n) {
    if (n < 2) {
        return n; 
    }
    return fib(n - 1) + fib(n - 2);
}
console.log(fib(0));  // 0
console.log(fib(1));  // 1
console.log(fib(2));  // 1
console.log(fib(3));  // 2
console.log(fib(4));  // 3
console.log(fib(5));  // 5
console.log(fib(6));  // 8

  显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。结果,fib(n) 的栈帧数的内存复杂度是O(2n) 。

  把递归改写成迭代循环形式改进。不过,也可以保持递归实现,但将其重构为满足优化条件的形式。为此可以使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:

"use strict";
// 基础框架 
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);
}

  这样重构之后,就可以满足尾调用优化的所有条件,再调用 fib(1000) 就不会对浏览器造成威胁了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ItDaChuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值