原文:http://exploringjs.com/es6/ch_tail-calls.htmlhtml
ECMAScript 6 提供了尾调用优化(tail call optimization)功能,以使得对某些函数的调用不会形成调用栈(call stack)的增加。本文解释了这项功能,以及其带来的好处。
粗略的来讲,若是当一个函数所作的最后一件事是调用了另外一个函数,然后者不须要返回调用者函数中再去作任何动做时;以及由此可知,在这种状况下没有调用者的额外信息须要被储存在调用栈(call stack)上,函数间的调用更像一种goto跳转的时候 -- 这种调用就被成为尾调用(tail call),此时使得内存栈再也不增加的行为就称为尾调用优化(TCO - tail call optimization)。es6
举个例子来更好的理解下TCO。首先说明一下是否用TCO的区别:bash
function id(x) {
return x; // (A)
}
function f(a) {
const b = a + 1;
return id(b); // (B)
}
console.log(f(2)); // (C)
复制代码
假设有一个JS引擎经过 存储本地变量并返回栈上的地址 来管理方法调用。该引擎会如何执行上述代码呢?
app
Step 1. 最初,栈上只有全局变量id
和f
。函数
栈会对当前做用域的状态(包括本地变量、参数等)进行编码,造成被称为“调用帧”(frame)的一块。
优化
Step 2. 在代码中的C行,f()
被调用:首先,将要return到的位置被记录在栈中;而后f
的参数a
被分配并执行。ui
栈如今看起来是这样的:共有两个调用帧,一个是位于底部的全局做用域,另外一个是其上方 的f()
。
this
Step 3. id()
在B行中被调用。再次造成了一个调用帧,包含了id
将要返回到的地址及其参数x
被分配和调用的值。编码
Step 4. 在行A,结果x
被返回。id
的调用栈被移除,执行过程跳转到其调用帧中存储的要return的位置,也就是行B。(处理返回值有多种途径,最多见的两种是将结果留在栈中和在寄存器中处理之,此处按下不表)spa
栈如今是这副模样的了:
Step 5. 在行B中,从id
中返回的值将继续返回给f
的调用者。照旧,最上面的调用帧被移除,执行过程跳转到要return的位置 -- 行C。
Step 6. 行C接收到返回值3
并完成打印工做。
function id(x) {
return x; // (A)
}
function f(a) {
const b = a + 1;
return id(b); // (B)
}
console.log(f(2)); // (C)
复制代码
回顾上个章节的过程,其实 step 5 是多余的。行B中发生的所有事情其实只不过是把id()
中返回的值传递给行C罢了。理想状况是,id()
能够自行完成这一步,而跳过二传手 step 5。
能够经过对行B的函数调用采起不同的实现方式来达成以上目的。栈在调用发生前是这样的:
检查此次调用就会发现,它是f()
的最后一个行为。一旦id()
完成,f()
剩余执行的惟一行为就是把前者的结果返回给自身的调用者。所以,f
中的变量就不须要了,其调用帧也就能够在此次调用以前被移除了。赋给id()
的将要return地址直接能够是f
的return地址,也就是行C了。在id()
执行期间,栈看起来就是这样的:
id()
返回了数值3
,或者能够说它为f()
返回了这个值;由于经过行C,该值被传递给了f
的调用者。
不难发现,行B的函数调用就是一个尾调用。这样的调用能够在栈0增加的状况下完成。要判断函数调用是不是尾调用,必须检查其是否处于尾部(好比最后一个行为)。下一章节将讲述如何作到。
咱们已经了解到尾调用能够被更有效率的执行,那么如何认定一个尾调用呢?
首先,调用函数的方式是无所谓的。下列调用若是出如今尾部,就均可以被优化:
func(···)
obj.method(···)
call()
: func.call(···)
apply()
: func.apply(···)
箭头函数能够用表达式做为方法体。对于尾调用优化,所以必须找出表达式中函数调用的尾部。只有下列表达式会包含尾调用:
? :
)||
)&&
),
)分别来举例看一下:
const a = x => x ? f() : g();
复制代码
f()
和 g()
都在尾部。
const a = () => f() || g();
复制代码
f()
不在尾部,g()
在尾部。至于为何,看看下面的等价代码就知道了:
const a = () => {
const fResult = f(); // not a tail call
if (fResult) {
return fResult;
} else {
return g(); // tail call
}
};
复制代码
逻辑或操做符的结果依赖于f()
的结果,因此是g()
,而非f()
的方法调用(调用者在其返回后又作了些什么)处于尾部。
const a = () => f() && g();
复制代码
一样,f()
不在尾部,g()
在尾部:
const a = () => {
const fResult = f(); // not a tail call
if (!fResult) {
return fResult;
} else {
return g(); // tail call
}
};
复制代码
理由和逻辑或相同。
const a = () => (f() , g());
复制代码
依然是,f()
不在尾部,g()
在尾部:
const a = () => {
f();
return g();
}
复制代码
对于声明语句,下列规则适用,只有这些混合声明语句会包含尾调用:
{}
界定,有时会有一个label)if
: 包括逻辑上的 “then” 和 “else” 子句do-while
, while
, for
: 在其循环体中switch
: 在其判断体中try-catch
: 只在 catch
子句中,try
子句将 catch
子句做为上下文,致使没法被优化try-finally
, try-catch-finally
: 只在 finally
子句中,它会成为其余子句的上下文对于全部原子(非混合)声明语句,只有return
会包含尾调用。其余此类声明语句都有没法被优化的上下文。以下所示,当expr
部分包含尾调用时,下列声明语句就包含尾调用。
return «expr»;
复制代码
在非严格模式下,大多数引擎会包含下面两个属性,以便开发者检查调用栈:
func.arguments
: 表示对 func
最近一次调用所包含的参数func.caller
: 引用对 func
最近一次调用的那个函数在尾调用优化中,这些属性再也不有用,由于相关的信息可能以及被移除了。所以,严格模式(strict mode)禁止这些属性,而且尾调用优化只在严格模式下有效。
下面的代码中,对bar()
的函数调用不算在尾部:
function foo() {
bar(); // this is not a tail call in JS
}
复制代码
缘由在于foo()
的最后一个动做不是对bar()
的函数调用,而是隐式的返回了undefined
。换句话说,foo()
的行为以下:
function foo() {
bar();
return undefined;
}
复制代码
调用者能够依赖一个老是返回undefined
的foo()
;但若是对bar()
作了尾调用优化,那么其返回值就有可能改变了foo
的行为。
所以,若是想要bar()
成为一个尾调用,就得改为这样:
function foo() {
return bar(); // tail call
}
复制代码
若是一个函数的主递归调用发生在尾部,那这个函数就是尾递归。
譬如,下面的阶乘函数不是尾递归,由于行A中的主递归调用不在尾部:
function factorial(x) {
if (x <= 0) {
return 1;
} else {
return x * factorial(x-1); // (A)
}
}
复制代码
能够用一个辅助方法facRec()
来使factorial()
成为尾递归。行A中的主递归调用处于尾部了:
function factorial(n) {
return facRec(n, 1);
}
function facRec(x, acc) {
if (x <= 1) {
return acc;
} else {
return facRec(x-1, x*acc); // (A)
}
}
复制代码
这样,一些非尾递归的函数就能够转化成尾递归了。
尾调用优化使得在递归循环中不增加调用栈成为可能。下面举两个例子。
function forEach(arr, callback, start = 0) {
if (0 <= start && start < arr.length) {
callback(arr[start], start, arr);
return forEach(arr, callback, start+1); // tail call
}
}
forEach(['a', 'b'], (elem, i) => console.log(`${i}. ${elem}`));
// Output:
// 0. a
// 1. b
复制代码
function findIndex(arr, predicate, start = 0) {
if (0 <= start && start < arr.length) {
if (predicate(arr[start])) {
return start;
}
return findIndex(arr, predicate, start+1); // tail call
}
}
findIndex(['a', 'b'], x => x === 'b'); // 1复制代码
----------------------------------------