JavaScript尾遞歸優化探索

原文地址: https://github.com/HolyZheng/...

尾調優化

在知道尾遞歸以前,咱們要直到什麼是尾調用優化,由於尾調用優化是尾遞歸的基礎。尾調用就是:在函數的最後一步調用另外一個函數。git

function f() {
  return g()
}
ps:最後一步必須是之久調用另外一函數,而不能是一個常量或是一個表達式,如 return y 或 return g() + 1

尾調用優化的原理是什麼?

按照阮一峯老師在es6的函數擴展中的解釋就是:函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到AB的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。es6

這裏的「調用幀」和「調用棧」,說的應該就是「執行環境」和「做用域鏈」。由於尾調用時函數的最後一部操做,因此再也不須要保留外層的調用幀,而是直接取代外層的調用幀,因此能夠起到一個優化的做用。github

尾遞歸優化

咱們知道,遞歸雖然使用起來方便,可是遞歸是在函數內部調用自身,當遞歸次數達到必定數量級的時候,他造成的調用棧的深度是很可怕的,極可能會發生「棧溢出」錯誤。尾遞歸優化,就是利用尾調用優化的原理,對遞歸進行優化。舉一個很常見的例子:
求斐波那契數值算法

function fibonacci (n) {
    return n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}

此函數沒有進行任何的優化,當咱們在控制檯去執行此函數的時候,fibonacci(40)就已經出現了明顯的響應慢的問題,fibonacci(50)的時候瀏覽器卡死。
優化瀏覽器

function fibonacci (n, ac1, ac2) {
    (ac1 = ac1 || 1), (ac2 = ac2 || 1);
    return n <= 1 ? ac2 :fibonacci(n - 1, ac2, ac1 + ac2);
}

此優化有兩個點:首先進行了算法上的優化,減小了不少重複的計算,時間複雜度大大下降;第二進行了尾遞歸優化,按理說不會發生「棧溢出」。咱們能夠到控制檯中再嘗試,發現速度的提高不是通常的快,證實第一個優化生效了,可是當咱們容許fibonacci(10000)的時候,報錯了:Uncaught RangeError: Maximum call stack size exceeded,這就說明咱們的尾遞歸優化並無生效。爲何呢?函數

侷限性

上面說到,咱們直接再瀏覽器的控制檯中執行fibonacci(10000)的時候,發生了棧溢出,這是爲何呢?關於這一點,我目前查閱資料以後的理解就是,雖然es6已經提出了要實現尾遞歸優化,可是真正落地實現了尾遞歸優化的瀏覽器並很少。因此當咱們使用尾遞歸進行優化的時候,依舊發生了「棧溢出」的錯誤。優化

蹦牀函數

那怎麼辦呢?咱們還有另外一個方法去達到尾遞歸優化的效果,那就是使用蹦牀函數(trampoline)code

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

代碼修改成返回一個新函數。blog

function fibonacci (n, ac1, ac2) {
    (ac1 = ac1 || 1), (ac2 = ac2 || 1);
    return n <= 1 ? ac2 :fibonacci.bind(null, n - 1, ac2, ac1 + ac2);
}

兩個函數結合就能夠將遞歸狀態爲循環,棧溢出的問題也就解決了。遞歸

trampoline(fibonacci (100000))
// Infinity
相關文章
相關標籤/搜索