理解尾调用优化

302 阅读4分钟

尾调用优化是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)
}

这样重构之后,就满足了尾调用优化的所有条件

参考:红宝书第四版