指某个函数的最后一步是return的是另一个函数调用,而不进行任何其他运算

    以下两种情况都不是尾调用:

    1. //情况1:
    2. function f(x){
    3. let y = g(x);
    4. return y;
    5. }
    6. //情况2:
    7. function f(x){
    8. return g(x) + 1;
    9. }

    尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

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

    1. function foo () { console.log(111); }
    2. function bar () { foo(); }
    3. function baz () { bar(); }
    4. baz();

    上面的每个函数在调用另一个函数的时候,并没有 return 该调用,所以JS引擎会认为你还没有执行完,会保留你的调用帧。

    但是如果更改成下面这样:

    1. function foo () { console.log(111); }
    2. function bar () { return foo(); }
    3. function baz () { return bar(); }
    4. baz();

    尾调用优化只在严格模式下有效。

    在非严格模式下,大多数引擎会包含下面两个属性,以便开发者检查调用栈:

    • func.arguments: 表示对 func最近一次调用所包含的参数

    • func.caller: 引用对 func最近一次调用的那个函数

    在尾调用优化中,这些属性不再有用,因为相关的信息可能以及被移除了。因此,严格模式(strict mode)禁止这些属性。尾调用优化后,每次return的内层函数的调用记录会取代外层函数的调用记录,调用栈中始终只保持了一条调用帧。

    函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

    递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生”栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生”栈溢出”错误。

    1. function factorial(n) {
    2. if (n === 1) return 1;
    3. return n * factorial(n - 1);
    4. }
    5. factorial(5) // 120

    上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

    如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

    1. function factorial(n, total) {
    2. if (n === 1) return total;
    3. return factorial(n - 1, n * total);
    4. }
    5. factorial(5, 1) // 120

    尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。

    理解标红文字很重要,很重要,很重要。

    可以通过下面的例子来加强理解:

    例子:
    使用尾递归:

    1. // 使用递归的方法,把一个数组反转。把需要返回的数组arr作为参数,每次传下去。最后直接返回这个参数arr即可。
    2. function reverse(A,arr=[]) {
    3. // 每次取出数组最后一项,拼接在arr后面
    4. return A.length ? reverse(A.slice(0,-1),arr.concat(A.slice(-1))) : arr
    5. }
    6. let A = [1,2,3,4,5,6]
    7. console.log(reverse(A)) // 6 5 4 3 2 1

    如果不使用尾递归可能需要这样:

    1. //方法1:需要定义额外的变量
    2. let arr = []
    3. function reverse(A) {
    4. arr = arr.concat(A.slice(-1))
    5. return A.length ? reverse(A.slice(0, -1)) : arr
    6. }
    7. //方法2:多次递归调用,调用记录一直在增加
    8. function reverse(A) {
    9. return A.length ? [...A.slice(-1),...reverse(A.slice(0, -1))] : A
    10. }

    尾递归的方法很明显更好。

    尾递归不一定会将你的代码执行速度提高;相反,可能会变慢。不过,尾递归可以让你使用更少的内存。

    尾调用和尾递归: https://juejin.im/post/5acdd7486fb9a028ca53547c