尾调用优化是ES6新增的一项内存管理优化机制,主要是对函数调用栈的管理。
函数调用栈
调用栈是解释器追踪函数执行流的一种机制,当执行环境中调用了多个,通过这种机制,我们可以追踪到当前执行的是哪个函数,执行的函数体中又执行了哪个函数
- 当调用一个函数时,解释器会将其入栈并执行
- 如果当前执行的函数内还有函数体,新函数也会被入栈,然后会立即执行
- 函数体执行完毕后,就会被清除出栈
- 当分配的调用栈空间被占满,就会发生堆栈溢出的错误
function foo() {
bar()
}
function bar() {
//dosomething
}
foo()
-
忽略前面的函数声明代码,一直到foo()
-
然后进入到foo函数内,foo函数被推到栈中并执行 当前调用栈
— foo
-
然后 bar(),进入到bar函数内,bar函数被推到栈中并执行 当前调用栈
— bar — foo
-
bar函数执行完毕,退出,回到foo函数,并且bar函数被清除出栈 当前调用栈
— foo
-
继续执行foo函数体内后面的代码
-
foo函数执行完毕,退出,foo函数被清除出栈
此时,我们又得到一个空空如也的调用栈
尾调用
即外部函数的返回值是一个内部函数的返回值
function foo() {
return bar()
}
尾调用优化
在ES6 优化之前,调用栈的变化同上面的例子相似,而在优化之后的会发生如下流程:
- 执行到foo函数,foo函数被入栈,
- 执行foo函数体,到达return语句,为求值返回语句,必须先求值bar
- 此时,引擎发现如果把foo清除栈也无所谓,因为bar的返回值也是foo的返回值(这一步是优化之前没有的)
- 将foo清除出栈
- 执行bar,将bar入栈,执行完之后,出栈
优化之前,每多一层嵌套的函数,就会多一个函数入栈,优化之后可以保持始终只有一个函数在栈中,当然,前提是符合尾调用优化的条件
尾调用优化的条件
尾调用优化的条件就是确定外部栈帧真的没有必要存在了,可以清除了,涉及的条件如下:
- 代码是在严格模式执行的
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
“use strict”
//无优化 尾调用没有返回
function foo() {
bar()
}
//无优化 尾调用没有直接返回
function foo() {
const res = bar()
return res
}
//无优化 尾调用返回后还要执行其他操作
function foo() {
return bar().toString()
}
//无优化。尾调用是一个闭包
function foo() {
const a = 1;
function bar() { return a }
return bar()
}
//有优化 栈帧销毁前。执行参数计算
function foo(a, b) {
return bar(a + b)
}
//有优化 初始返回值不涉及栈帧
function foo(a, b) {
if (a < b) {
return bar(a + b)
}
}
//有优化 两个内部函数都在尾部
function foo(value) {
return value ? bar() : boo();
}
无论是递归尾调用还是非递归尾调用,都可以进行这个优化,引擎并不区分尾调用中调用的是自身还是其他的函数,这个优化在递归场景下优化的效果是最明显的,因为递归代码最容易在栈中迅速产生大量栈帧
Ps: 为什么要求是在严格模式下,因为在非严格模式下,函数调用中允许使用f.arguments和f.caller,而他们都会引用外部函数的栈帧,所以必须要求在严格模式下,防止引用这些属性
尾调用优化的应用
function fib(n) {
if (n < 2) {
return n
}
return fib(n - 1) + fib(n - 2)
}
这是一个计算斐波那契数列的函数,最后返回的两个函数进行了相加的操作,不符合尾调用优化的条件
“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)
}
这样重构之后,就满足了尾调用优化的所有条件
参考:红宝书第四版