ES6學習筆記 -- 尾調用優化

什麼是尾調用?es6

尾調用(Tail Call)是函數式編程的一個重要概念,就是指某個函數的最後一步是調用另外一個函數。編程

function f(x) {
    return g(x)
}

如上,函數 f 的最後一步是調用函數g,這就叫作尾調用。編程語言

可是,以下狀況並不屬於尾調用:函數式編程

// 狀況一
function f(x) {
    let y = g(x);
    return y;
}

// 狀況二
function f(x) {
    return g(x) + 1;
}

// 狀況三
function f(x) {
    g(x);
}

一、調用g以後,還有賦值操做,因此不屬於尾調用,即便語義徹底同樣;二、屬於調用後還有操做,即便寫在一行內,也不屬於尾調用;三、等同於 function(x) { g(x); return undefined; } , return undefined纔是它的最後執行語句。函數

可是,尾調用其實不必定出如今尾部,只要是最後一步操做便可:優化

function f(x) {
    if (x > 0) {
        return m(x)
    }
    return n(x)
}

如上,m 和 n 都屬於尾調用,由於它們都是函數 f 的最後一步操做。spa

尾調用優化code

尾調用之因此與其餘調用不一樣,就在於它的特殊的調用位置。blog

函數調用會在內存造成一個」調用記錄「,又稱」調用幀「(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還一個C的調用幀,以此類推。全部的調用幀,就造成一個」調用棧「 (call stack)。遞歸

尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接內層函數的調用幀,取代外層函數的調用幀就能夠了。

function f() {
    let m = 1;
    let n = 2;
    return g(m + n);
}
f();

// 等同於
function f() {
    return g(3);
}
f();

// 等同於
g(3)

如上,若是函數g不是尾調用,函數f 就須要保存內部變量m 和 n 的值、g的調用位置等信息。可是因爲調用g以後,函數 f 就結束了因此執行到最後一步,徹底能夠刪除 f(x) 的調用幀,只保留g(3) 的調用幀。

這就叫作」尾調用優化「(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存。這就是」尾調用優化「的意義。

注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行」尾調用優化「。

function addOne(a) {
    var one = 1;
    function inner(b) {
        return b + one;
    }
    return inner(a);
}

上面的函數不會進行尾調用優化,由於內層函數inner用到了外層函數addOne的內部變量one。

尾遞歸

 函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。

遞歸是一個很是耗內存的操做,由於須要同時保存成千上百個調用幀,很容易發生」棧溢出「錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生」棧溢出「錯誤。

function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}

factorial(5) // 120

如上是一個階乘函數,計算n的階乘,最多須要保存n 個調用記錄,複雜度O(n)。

但,若是改爲尾遞歸,只保留一個調用記錄,複雜度O(1)。

function factorial(n, total) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
}

factorial(5, 1)  // 120

還有一個比較著名的例子,就是計算Fibonacci(斐波那契數列) 數列 ,也能充分說明尾遞歸優化的重要性。

非尾遞歸的Fibonacci 數列實現以下:

function Fibonacci(n) {
    if (n <= 1) {return 1};
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆棧溢出
Fibonacci(500) // 堆棧溢出

尾遞歸優化過的Fibonacci 數列實現以下:

function Fibonacci2(n, ac1 = 1, ac2 = 1) {
    if (n <= 1) { return ac2 };
    return Fibonacci2( n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 堆棧未溢出, 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

因而可知,」尾調用優化「對遞歸操做意義重大,因此一些函數式編程語言將其寫入了語言規格。ES6就是如此,第一次明確規定,全部ECMAScript的實現,都必須部署」尾調用優化「。這就是說,ES6中只要使用尾遞歸,就不會發生棧溢出,相對節省內存。

 

推薦阮一峯老師的詳細文章:http://es6.ruanyifeng.com/#docs/function

相關文章
相關標籤/搜索