尾呼叫及遞迴優化
ES6 規範中添加了對尾呼叫優化的支援,雖然目前來看支援情況還不是很好,但瞭解其原理還是很有必要的,有助於我們編寫高效的程式碼,一旦哪天引擎支援該優化了,將會收穫效能上的提升。 討論尾呼叫前,先看函式正常呼叫時其形成的堆疊(stack frame)情況。 函式的呼叫及呼叫堆疊先看一個概念:呼叫堆疊(call stack),或叫呼叫幀(stack frame)。 我理解的呼叫堆疊:因為函式呼叫後,流程會進入到被呼叫函式體,此時需要傳入一些入參,這些入參需要存放到一個位置,另外當函式呼叫結束後,執行流程需要返回到被呼叫的地方,這個返回的地方也需要被記錄下來。所以會為這次函式呼叫分配一個儲存區域,存放呼叫時傳入的入參,呼叫結束後返回的地址。這個儲存區域儲存的都是跟本次函式呼叫相關的資料,所以,它是該函式的呼叫堆疊。 考察下面的示例程式碼: function g(x) { return x; // (C) } function f(a) { let b = a + 1; return g(b); // (B) } console.log(f(2)); // (A) 下面模擬 JaavScript 引擎在執行上面程式碼時的流程,
尾呼叫如果一個函式最後一步是呼叫另一個函式,則這個呼叫為尾呼叫。比如: function g(x) { return x; } function f(y) { var a = y + 1; return g(a); } 但下面這個就不算尾呼叫了: function g(x) { return x; } function f(y) { var a = y + 1; return g(a) + 1; } 因為這裡 function g(x) { return x; } function f(y) { var a = y + 1; var tmp = g(a); var result = tmp + 1; return result; } 經過改造後就能很明顯看出,其中對 尾呼叫的判定函式的呼叫形式首先,JavaScript 中呼叫函式是多樣的,只要這是另一函式中最後一步操作,都可稱作尾呼叫。比如以下的函式呼叫形式:
表示式中的尾呼叫因為剪頭函式的函式體可以是表示式,所以表示式最終的步驟是什麼決定了該剪頭函式中是否包含尾呼叫。 能夠形成尾呼叫的表示式有如下這些,
const a = x => x ? f() : g(); 其中
const a = () => f() || g(); 上面示例中, const a = () => { let fResult = f(); // not a tail call if (fResult) { return fResult; } else { return g(); // tail call } };
所以從上面的轉換中看出,對於
const a = () => f() && g(); 與邏輯或表示式雷同,這裡需要對 const a = () => { let fResult = f(); // not a tail call if (!fResult) { return fResult; } else { return g(); // tail call } };
const a = () => (f() , g()); 這個就比較好理解了,逗號表示式是依次執行的,整個表示式返回結果為最後一個表示式。所以這裡 需要注意的是,單獨的函式呼叫並不是尾呼叫,比如下面這樣: function foo() { bar(); // this is not a tail call in JS } 這裡 function foo() { bar(); return undefined; } 像這種情況其實並不能簡單地通過加一個 尾呼叫優化回到最開始的那個示例: function g(x) { return x; // (C) } function f(a) { let b = a + 1; return g(b); // (B) } console.log(f(2)); // (A) 這裡 如果仔細看前面呼叫過程的分析,會發現,在 所以,完全可以省掉 上面優化後的執行場景下,其呼叫堆疊的分配則變成了:
最最開始不同之處在於,在建立 其好處顯而易見,在函式連續呼叫過程中,堆疊數沒有增加。假如不止一次尾呼叫, 利用這個特性,我們可以將一些不是尾呼叫的函式想辦法改成尾呼叫,達到優化呼叫堆疊的目的,這便是尾呼叫優化。 尾遞迴呼叫如果函式的尾呼叫是對自己的呼叫,便形成了遞迴呼叫,同時還是歸呼叫,所以合稱 尾遞迴呼叫。相比於傳統的遞迴,呼叫堆疊極速增加的不同,尾遞迴呼叫的呼叫堆疊是恆定的,這由前面的分析可知。 所以尾呼叫優化特別適合用於遞迴的情況,收益會很大。 計算階乘就是典型的遞迴場景: function factorial(x) { if (x <= 0) { return 1; } else { return x * factorial(x-1); // (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 相關資源
|