什么是尾调用?
某个函数的最后一步调用另一个函数
1 | function call1(a) { |
尾调用优化
注意:尾调用优化是否方便可行取决于运行环境对此类优化的支持度
函数调用会在内存中形成一个‘调用记录’,又称为‘调用帧’。用来保存调用位置和内存变量等信息。如果A函数内部调用B函数,在A的调用记录上,会生成一个B的调用记录,等到B运行结束,将结果返回给A,B的调用记录才会消失。如果B的内部还调用C,那么在B上面会形成一个C的调用记录,以此类推。所有的调用记录,形成了一个‘调用栈’(call stack)。

但是,尾调用由于是函数调用的最后一步操作,所以不需要保存外部函数的调用记录,因为调用位置、内部变量等信息都不会再用到,只要直接用内层函数的调用记录,取代外层函数调用记录就可以了。所以,如果所有函数都是尾调用,那么每次执行的时候,只需要记录一项调用记录,从而节省大量内存,即‘尾调用优化’。
注意:只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行‘尾调用优化’
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存许多调用记录,很容易发生‘栈溢出’错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生‘栈溢出’错误。
1 | // 阶乘计算 |
递归函数的改写优化
尾递归的实现,需要将所有用到的内部参数变量改写成函数的参数,不过直接修改的函数往往不太直观,一般采用以下两种方法解决:
(1) 除尾递归函数外,在提供一个正常形式的函数,通过正常表现的函数调用递归函数
1 | function tailCall(n, total) { |
(2) 使用柯理化,将多参数的函数转化为单参数形式
1 | function currying(fn, n) { |
ES6的尾调用优化只在严格模式下开启,正常模式无效
正常模式下,函数内部有两个变量,可以跟踪函数的调用栈
- arguments 返回调用时函数的参数
- func.caller 返回调用当前函数的那个函数
尾调用优化发生时,函数调用栈会改写,上面两个变量会失真。严格模式禁用这两个变量,所以尾调用仅在严格模式下生效。