尾调用
尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
需要注意的是最后一步不是指最后一行代码,甚至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条,原因是没有开启严格模式,如果开启严格模式的话,禁止调式了,无法跟踪调用栈。