尾调用

尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

需要注意的是最后一步不是指最后一行代码,甚至return fn() - 1,即使在同一行,也不是尾调用。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f()的调用记录,只保留g(3)的调用记录。

这就叫做”尾调用优化”(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是”尾调用优化”的意义。

尾递归

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

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

function fn(n){
if(n===1) return 1
return n * fn(n-1)
}

// 改成尾递归
function fn(n,total){
    if(n===1) return total
    return fn(n-1,n*total)
}

递归函数的改写:

就是把内部变量改写成函数的参数。上述中需要用到的是total变量,把它改成函数的参数就可以。但是不太直观。这个时候可以利用柯里化currying,它的意思就是将多参数的函数转换成单参数的函数。例如:

function currying(fx(),n){
    return function(m){
        return fx.call(this,m,n) // call用来绑定作用域,防止乱跑,绑定的是f的作用域,m是f的函数参数,这个例子可以不用call
    }
}

function fn(n,total){
    if(n===1) return total
    return fn(n-1,n*total) 
}

const f  = currying(fn,1)
f(5)  // 120

递归的本质是循环,循环可以用递归代替,但是用递归,就最好使用尾递归。

严格模式

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

arguments:返回调用时函数的参数。

func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真(不真实,看起来没有优化,如下图)。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

看起来函数调用记录为5条,原因是没有开启严格模式,如果开启严格模式的话,禁止调式了,无法跟踪调用栈。