尾调用
函数的最后一步是调用其他函数,只要是函数的最后一步操作是调用其他函数都可以算是尾调用。
function f(x){
if(x > 0){
return m(x)
}else {
return n(x)
}
}
// 上面的m(x),n(x)都属于尾调用
由于函数的调用会形成调用栈,若A函数内部还有B函数执行,则在A函数的调用帧上面会有B函数的调用帧,依次类推,就形成了调用栈。
尾调用优化
会大大节省内存,由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。只有不再用到外层函数的内部变量,内部函数的调用帧才会取代外层函数调用帧。
function (){
let num = 1
function fn(a){
return a + num
}
return fn(1)
}
// 不属于尾调用优化,由于fn函数使用了外部函数的变量num
尾递归
尾调用自身,就称为尾递归。由于递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
// 普通递归
function fibonacci(n){
if(n <= 1) return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}
console.time()
fibonacci(10) //89 default: 0.03515625 ms
fibonacci(100) //超时
console.timeEnd()
//尾递归
function fibonacci2(n,val = 1,val2 = 1){
if(n <= 1) return val2
// 使用es6的默认赋值方式,将值作为函数的参数,这样就没有使用外部函数内的变量,从而实现取代外部函数调用帧
return fibonacci2(n - 1, val2, val + val2)
}
console.time()
fibonacci2(10) //89 default: 0.03515625 ms
fibonacci2(100) //573147844013817200000 default: 0.0478515625 ms
console.timeEnd()
严格模式
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments
:返回调用时函数的参数。func.caller
:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效
尾递归优化实现
尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”
function tco(fn){
let value;
let active = false //是否进入尾递归
let arr = []
return function accumulator(){
arr.push(arguments) //保存每一轮递归执行的参数,所以循环总是会执行的
if(!active){
avtive = true
while(arr.length){ //此处采用循环
value = fn.apply(this,arr.shift())
}
active = false
return value
}
}
}
var sum = tco(function(x,y){
if(y>0){
return sum(x+1,y-1) //每次递归返回的undefined,所以避免了递归执行
}else {
return x
}
})
sum(1,1000) // 100001