简单地说,一个“尾部调用”是一个出现在另一个函数“尾部”的函数调用,于是在这个调用完成后,就没有其他的事情要做了(除了也许要返回结果值)。

例如,这是一个带有尾部调用的非递归形式:

是一个在bar(..)中的尾部调用,因为在foo(..)完成之后,bar(..)也即而完成,除了在这里需要返回foo(..)调用的结果。然而,bar(40) 不是 一个尾部调用,因为在它完成后,在baz()能返回它的结果前,这个结果必须被加1。

然而,如果一个支持TCO的引擎可以认识到foo(y+1)调用位于 尾部位置 意味着bar(..)基本上完成了,那么当调用foo(..)时,它就并没有必要创建一个新的栈帧,而是可以重复利用既存的bar(..)的栈帧。这不仅更快,而且也更节省内存。

在一个简单的代码段中,这种优化机制没什么大不了的,但是当对付递归,特别是当递归会造成成百上千的栈帧时,它就变成了 相当有用的技术。引擎可以使用TCO在一个栈帧内完成所有调用!

在JS中递归是一个令人不安的话题,因为没有TCO,引擎就不得不实现一个随意的(而且各不相同的)限制,规定它们允许递归栈能有多深,来防止内存耗尽。使用TCO,带有 尾部位置 调用的递归函数实质上可以没有边界地运行,因为从没有额外的内存使用!

  1. function fact(n,res) {
  2. if (n < 2) return res;
  3. return fact( n - 1, n * res );
  4. }
  5. return fact( n, 1 );
  6. }
  7. factorial( 5 ); // 120

这个版本的factorial(..)仍然是递归的,而且它还是可以进行TCO优化的,因为两个内部的调用都在 尾部位置

注意: 一个需要注意的重点是,TCO尽在尾部调用实际存在时才会实施。如果你没用尾部调用编写递归函数,性能机制将仍然退回到普通的栈帧分配,而且引擎对于这样的递归的调用栈限制依然有效。许多递归函数可以像我们刚刚展示的factorial(..)那样重写,但是要小心处理细节。

ES6要求各个引擎实现TCO而不是留给它们自行考虑的原因之一是,由于对调用栈限制的恐惧,缺少TCO 实际上趋向于减少特定的算法在JS中使用递归实现的机会。

ES6保证,从现在开始,JS开发者们能够在所有兼容ES6+的浏览器上信赖这种优化机制。这是JS性能的一个胜利!