js函数尾调用

1,153 阅读4分钟

函数尾调用

  • 再先解释尾调用之前,要说一下复习一下函数的作用域,作用域链和调用栈等关于函数的一些基础知识

函数作用域和作用域链

函数调用栈

  • 函数调用会在内存中形成一个调用记录,又称为调用帧,保存调用信息和内部变量等。如果在函数A中调用函数B,那么在A的调用帧上方还会形成一个B的调用帧。等到B调用结束后,将结果返回A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

js函数调用

  • 如果没有return语句,默认会返回undefined,即执行最后一步是return undefined

执行环境和作用域

  • 执行环境定义了变量或者函数有权访问的其它数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的函数和变量都保存在这个变量对象中。
  • 全局执行环境是最外围的执行环境,在web浏览器中,winnow对象被认为时全局执行环境,所有的全局函数和变量都是作为window的方法和属性创建的。
  • 函数也有单独的执行环境。当执行流进入一个函数时,函数的变量环境会被推入一个环境栈,执行完毕后,栈将其环境弹出,控制权返回给之前的执行环境。
  • 当代码在执行环境中执行时,会创建变量对象的一个作用域链。++作用域链的用途是保证对执行环境有权访问的变量和函数的有序访问++。作用域链的最前端始终时当前执行代码所处的环境,最外层时全局执行环境。如果当前环境时函数,则将其活动对象作为变量对象,活动对象最开始时只包括一个变量,即arguments对象(全局环境中不存在)。
  • 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索从作用域链的最前端开始,然后逐级向后回溯,直到找到标识符为止,如果找不到,则会报错。

什么是函数尾调用?

  • 简单的来说,函数尾调用就是在函数执行的最后一步是调用另外一个函数
// 正确
function a () {
    return b()
}
// 错误一
function a () {
    const x = b()
    return x
}
// 错误二
function a () {
    return b() + 1
}
// 错误三
function a () {
    b()
}
  • 以上三个错误原因分析
    1. 错误一,b执行后赋值给了x
    2. 错误二,return b() + 1其实可以拆解为const x = b(); x = x + 1 return x;,因此在函数调用后还有赋值操作
    3. 错误三,根据在js函数调用中,最后一步是return undefined

函数的尾调用优化

  • 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
  • ++只有不再用到外层函数的内部变量++,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”

什么情况下使用尾调用会有优化

  • 在对尾调用有优化的环境中,目前只有safari
  • es6中严格模式下采用开启尾调用优化

尾递归

为什么要使用尾递归

  • 递归函数由于要保留很多的函数调用栈,所有会非常耗费内存。如果使用了尾递归,则只需要保留一个调用栈,所以可以说永远不会发生“栈溢出”的错误。

递归函数改写为尾递归(实例)

  1. 计算n的阶乘
function factorical (n, total = 1) {
    if(n === 1) return 1;
    return factorical(n - 1, n * total)
}
  1. 斐波纳切数列
function fibonacci (n, ac1 = 0, ac2 = 1) {
    if (n === 0) return ac1;
    if (n === 1) return ac2;
    return fibonacci(n - 1, ac2, ac1 + ac2)
}

非严格模式下如何实现尾递归优化

  • ++使用“遍历”代替“递归”++

蹦床函数(不是真正的尾递归优化)

function trampoline(f) {
    while (f && f instanceof Function ) {
        f = f()
    }
    return f;
}
  • 现在有一个递归函数
function sum (x, y) {
    if (y > 0) {
        // 改写前, 会导致栈溢出
        return sum(x + 1, y - 1)
        // 改写后,返回一个函数
        return sum.bind(null, x + 1, y - 1)
    } else {
        return x
    }
}
trampoline(sum(1, 10000)) // 10001

尾递归优化

function tco (f) {
    var value
    var flag = false
    var accumulated = []
    
    return function () {
        accumulated.push(agruments)
        if (!active) {
            while (accumulated.length) {
                value = f.apply(null, accumulated.shift())
            }
        }
        active = true
        return value
    }
}
var sum = tco(function () {
    if (y > 0) {
        // 改写前, 会导致栈溢出
        return sum(x + 1, y - 1)
    } else {
        return x
    }
})
sum(1, 10000)