简单地说,一个“尾部调用”是一个出现在另一个函数“尾部”的函数调用,于是在这个调用完成后,就没有其他的事情要做了(除了也许要返回结果值)。
例如,这是一个带有尾部调用的非递归形式:
是一个在bar(..)
中的尾部调用,因为在foo(..)
完成之后,bar(..)
也即而完成,除了在这里需要返回foo(..)
调用的结果。然而,bar(40)
不是 一个尾部调用,因为在它完成后,在baz()
能返回它的结果前,这个结果必须被加1。
然而,如果一个支持TCO的引擎可以认识到foo(y+1)
调用位于 尾部位置 意味着bar(..)
基本上完成了,那么当调用foo(..)
时,它就并没有必要创建一个新的栈帧,而是可以重复利用既存的bar(..)
的栈帧。这不仅更快,而且也更节省内存。
在一个简单的代码段中,这种优化机制没什么大不了的,但是当对付递归,特别是当递归会造成成百上千的栈帧时,它就变成了 相当有用的技术。引擎可以使用TCO在一个栈帧内完成所有调用!
在JS中递归是一个令人不安的话题,因为没有TCO,引擎就不得不实现一个随意的(而且各不相同的)限制,规定它们允许递归栈能有多深,来防止内存耗尽。使用TCO,带有 尾部位置 调用的递归函数实质上可以没有边界地运行,因为从没有额外的内存使用!
function fact(n,res) {
if (n < 2) return res;
return fact( n - 1, n * res );
}
return fact( n, 1 );
}
factorial( 5 ); // 120
这个版本的factorial(..)
仍然是递归的,而且它还是可以进行TCO优化的,因为两个内部的调用都在 尾部位置。
注意: 一个需要注意的重点是,TCO尽在尾部调用实际存在时才会实施。如果你没用尾部调用编写递归函数,性能机制将仍然退回到普通的栈帧分配,而且引擎对于这样的递归的调用栈限制依然有效。许多递归函数可以像我们刚刚展示的factorial(..)
那样重写,但是要小心处理细节。
ES6要求各个引擎实现TCO而不是留给它们自行考虑的原因之一是,由于对调用栈限制的恐惧,缺少TCO 实际上趋向于减少特定的算法在JS中使用递归实现的机会。
ES6保证,从现在开始,JS开发者们能够在所有兼容ES6+的浏览器上信赖这种优化机制。这是JS性能的一个胜利!