原文: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複製代碼
----------------------------------------