调用栈 JS执行时会形成调用栈,调用一个函数时,返回地址、参数、本地变量都会被推入栈中,如果当前正在运行的函数a调用函数b,则函数b相关内容也会被推入栈顶。函数a执行完毕后,与a相关的内容都会被弹出调用栈。由于复杂类型值存放于堆中,因此弹出的只是指针,他们的值依然在堆中,由GC决定回收。
换而言之,函数调用会在内存中形成一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息。如果在函数a的内部调用函数b,那么在a的调用帧上方,还会形成一个b的调用帧。等到b运行结束,将结果返回到a,b的调用帧才会消失。如果函数b的内部还调用函数c,那就还有一个c的调用帧,以此类推。所有的调用帧就形成一个“调用栈”。
尾调用 尾调用是指某个函数的最后一步是调用另一个函数。
尾调用优化 看下面的例子:
function g (item){
return item
}
function f(){
let m = 1
let n = 2
return g(m + n)
}
f()
// 上面的例子实际等同于 g(3)
复制代码
上面的代码中,如果函数g不是函数f的尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但是由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除函数f的调用帧,只保留g(3)的调用帧。 这就叫“尾调用优化”,即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这样大大节省内存,这就是“尾调用优化”的意义。 注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”。
function addOne(a){
let one = 1
function inner(b){
return b + one
}
return inner(a)
}
复制代码
显而易见,上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。
尾递归 递归就是函数调用自身,如果尾调用的函数是函数自身,就称为尾递归。 递归非常消耗内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”,但是对于尾递归来说,只存在一个调用栈,就不会发生“栈溢出”错误