尾调用
一、认识
尾调用 Tail Call
, 指的是在一个函数的最后一步直接调用另一个函数(或者自己),并将这个调用的返回结果直接返回给调用者。 尾调用 Tail Call
在尾调用的位置,当前函数不需要保留自己的执行上下文(例如局部变量、后续操作等),因此理论上可以复用当前的栈帧。如果 JavaScript
引擎实现了尾调用优化(Tail Call Optimization, TCO
),那么尾调用就可以避免调用栈不断增长的问题。因此 尾调用 不会在调用栈上增加新的堆栈帧(不会增加调用栈的长度),而是直接更新调用栈,调用栈所占空间始终是常量,节省了内存,避免了爆栈的可能性。所以, 我们在开发中, 尽量采用尾递归写法, 当需要递归操作时,可以重构函数,使递归调用处于尾调用位置。例如,在计算累加和、遍历树结构等场景下。
ES6
规范要求:ECMAScript 2015(ES6)
规范中要求实现尾调用优化,以便在尾调用位置复用调用栈。但这要求是在严格模式下进行的。尽管理论上 ES6
规范支持尾调用优化,但目前主流的 JavaScript
引擎(如 V8
、SpiderMonkey
等)并未普遍实现这一特性。因此,在实际开发中,尽管我们可以采用尾递归的写法,但不能完全依赖底层引擎来保证优化效果。
二、语法
2.1 直接尾调用
function foo1(){
return foo2(); // foo1 函数的最后一步调用另一个函数
}
function foo2(){
console.log("foo2 函数")
}
2.2 三元尾调用
const a = x => x ? f() : g()
// 改写成:
const a = x => {
if (x) {
return f()
} else {
return g()
}
}
可见 f
和 g
的返回值都是直接被返回的,符合尾调用的定义。
2.3 逻辑尾调用
const a = () => f() || g()
// 改写成:
const a = () => {
const result = f()
if (result) {
return result
} else {
return g()
}
}
||
运算符的结果依赖于 f
函数的返回值,而不是直接返回 f
的返回值,直接返回的只有 g
函数的返回值。说明, f
函数也不在尾递归位置上,而 g
函数在尾递归位置上
2.4 逗号尾调用
const a = () => (f(), g())
// 改写成:
const a = () => {
f()
return g()
}
可见,在尾递归位置上的仍然只有一个 g 函数。
2.5 非尾调用语法
-
单独的函数调用不是尾调用
function foo() { bar()}
// 改写成:
function foo() {
bar()
return undefined
}可以看到
return
语句返回的只是一个undefined
而并非bar
函数的返回值,所以这里不存在尾调用。 -
外部函数的最后一步是调用内部的函数并返回
function foo(){
function bar(){
}
return bar();
}函数返回一个内部函数并调用, 不是一个尾调用。