跳到主要内容

尾调用

2024年03月12日
柏拉文
越努力,越幸运

一、认识


尾调用 Tail Call, 指的是在一个函数的最后一步直接调用另一个函数(或者自己),并将这个调用的返回结果直接返回给调用者。 尾调用 Tail Call 在尾调用的位置,当前函数不需要保留自己的执行上下文(例如局部变量、后续操作等),因此理论上可以复用当前的栈帧。如果 JavaScript 引擎实现了尾调用优化(Tail Call Optimization, TCO),那么尾调用就可以避免调用栈不断增长的问题。因此 尾调用 不会在调用栈上增加新的堆栈帧(不会增加调用栈的长度),而是直接更新调用栈,调用栈所占空间始终是常量,节省了内存,避免了爆栈的可能性。所以, 我们在开发中, 尽量采用尾递归写法, 当需要递归操作时,可以重构函数,使递归调用处于尾调用位置。例如,在计算累加和、遍历树结构等场景下。

ES6 规范要求:ECMAScript 2015(ES6) 规范中要求实现尾调用优化,以便在尾调用位置复用调用栈。但这要求是在严格模式下进行的。尽管理论上 ES6 规范支持尾调用优化,但目前主流的 JavaScript 引擎(如 V8SpiderMonkey 等)并未普遍实现这一特性。因此,在实际开发中,尽管我们可以采用尾递归的写法,但不能完全依赖底层引擎来保证优化效果。

二、语法


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()
}
}

可见 fg 的返回值都是直接被返回的,符合尾调用的定义。

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 非尾调用语法

  1. 单独的函数调用不是尾调用

    function foo() {  bar()}

    // 改写成:

    function foo() {
    bar()
    return undefined
    }

    可以看到 return 语句返回的只是一个 undefined 而并非 bar 函数的返回值,所以这里不存在尾调用。

  2. 外部函数的最后一步是调用内部的函数并返回

    function foo(){
    function bar(){

    }
    return bar();
    }

    函数返回一个内部函数并调用, 不是一个尾调用。