溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JavaScript調用棧、尾遞歸和手動優化的示例分析

發布時間:2021-08-06 09:38:08 來源:億速云 閱讀:227 作者:小新 欄目:web開發

這篇文章給大家分享的是有關JavaScript調用棧、尾遞歸和手動優化的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

調用棧(Call Stack)

調用棧(Call Stack)是一個基本的計算機概念,這里引入一個概念:棧幀。

棧幀是指為一個函數調用單獨分配的那部分??臻g。

當運行的程序從當前函數調用另外一個函數時,就會為下一個函數建立一個新的棧幀,并且進入這個棧幀,這個棧幀稱為當前幀。而原來的函數也有一個對應的棧幀,被稱為調用幀。每一個棧幀里面都會存入當前函數的局部變量。

JavaScript調用棧、尾遞歸和手動優化的示例分析

當函數被調用時,就會被加入到調用棧頂部,執行結束之后,就會從調用棧頂部移除該函數。并將程序運行權利(幀指針)交給此時棧頂的棧幀。這種后進后出的結構也就是函數的調用棧。

而在JavaScript里,可以很方便的通過console.trace()這個方法查看當前函數的調用幀

JavaScript調用棧、尾遞歸和手動優化的示例分析

尾調用

說尾遞歸之前必須先了解一下什么是尾調用。簡單的說,就是一個函數執行的最后一步是將另外一個函數調用并返回。

以下是正確示范:

// 尾調用正確示范1.0
function f(x){
 return g(x);
}

// 尾調用正確示范2.0
function f(x) {
 if (x > 0) {
  return m(x)
 }
 return n(x);
}

1.0程序的最后一步即是執行函數g,同時將其返回值返回。2.0中,尾調用并不是非得寫在最后一行中,只要執行時,是最后一步操作就可以了。

以下是錯誤示范:

// 尾調用錯誤示范1.0
function f(x){
 let y = g(x);
 return y;
}

// 尾調用錯誤示范2.0
function f(x){
 return g(x) + 1;
}
// 尾調用錯誤示范3.0
function f(x) {
 g(x); // 這一步相當于g(x) return undefined
}

1.0最后一步為賦值操作,2.0最后一步為加法運算操作,3.0隱式的有一句return undefined

尾調用優化

在調用棧的部分我們知道,當一個函數A調用另外一個函數B時,就會形成棧幀,在調用棧內同時存在調用幀A和當前幀B,這是因為當函數B執行完成后,還需要將執行權返回A,那么函數A內部的變量,調用函數B的位置等信息都必須保存在調用幀A中。不然,當函數B執行完繼續執行函數A時,就會亂套。

那么現在,我們將函數B放到了函數A的最后一步調用(即尾調用),那還有必要保留函數A的棧幀么?當然不用,因為之后并不會再用到其調用位置、內部變量。因此直接用函數B的棧幀取代A的棧幀即可。當然,如果內層函數使用了外層函數的變量,那么就仍然需要保留函數A的棧幀,典型例子即是閉包。

在網上有很多關于講解尾調用的博客文章,其中流傳廣泛的一篇中有這樣一段。我不是很認同。

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() 的調用記錄,只保留 g(3) 的調用記錄。

但我認為第一種中,也是先執行m+n這步操作,再調用函數g同時返回。這應當是一次尾調用。同時m+n的值也通過參數傳入函數g內部,并沒有直接引用,因此也不能說需要保存f內部的變量的值。

總得來說,如果所有函數的調用都是尾調用,那么調用棧的長度就會小很多,這樣需要占用的內存也會大大減少。這就是尾調用優化的含義。

尾遞歸

遞歸,是指在函數的定義中使用函數自身的一種方法。函數調用自身即稱為遞歸,那么函數在尾調用自身,即稱為尾遞歸。

最常見的遞歸,斐波拉契數列,普通遞歸的寫法:

function f(n) {
 if (n === 0 || n === 1) return n 
 else return f(n - 1) + f(n - 2)
}

這種寫法,簡單粗暴,但是有個很嚴重的問題。調用棧隨著n的增加而線性增加,當n為一個大數(我測了一下,當n為100的時候,瀏覽器窗口就會卡死。。)時,就會爆棧了(棧溢出,stack overflow)。這是因為這種遞歸操作中,同時保存了大量的棧幀,調用棧非常長,消耗了巨大的內存。

接下來,將普通遞歸升級為尾遞歸看看。

function fTail(n, a = 0, b = 1) { 
 if (n === 0) return a
 return fTail(n - 1, b, a + b)
}

很明顯,其調用棧為

復制代碼 代碼如下:


fTail(5) => fTail(4, 1, 1) => fTail(3, 1, 2) => fTail(2, 2, 3) => fTail(1, 3, 5) => fTail(0, 5, 8) => return 5

被尾遞歸改寫之后的調用棧永遠都是更新當前的棧幀而已,這樣就完全避免了爆棧的危險。

但是,想法是好的,從尾調用優化到尾遞歸優化的出發點也沒錯,然并卵:),讓我們看看V8引擎官方團隊的解釋

Proper tail calls have been implemented but not yet shipped given that a change to the feature is currently under discussion at TC39.

意思就是人家已經做好了,但是就是還不能不給你用:)嗨呀,好氣喔。

當然,人家肯定是有他的正當理由的:

  1. 在引擎層面消除尾遞歸是一個隱式的行為,程序員寫代碼時可能意識不到自己寫了死循環的尾遞歸,而出現死循環后又不會報出stack overflow的錯誤,難以辨別。

  2. 堆棧信息會在優化的過程中丟失,開發者調試非常困難。

道理我都懂,但是不信邪的我拿nodeJs(v6.9.5)手動測試了一下:

JavaScript調用棧、尾遞歸和手動優化的示例分析

好的,我服了

手動優化

雖然我們暫時用不上ES6的尾遞歸高端優化,但遞歸優化的本質還是為了減少調用棧,避免內存占用過多,爆棧的危險。而俗話說的好,一切能用遞歸寫的函數,都能用循環寫——尼克拉斯·夏,如果將遞歸改成循環的話,不就解決了這種調用棧的問題么。

方案一:直接改函數內部,循環執行

function fLoop(n, a = 0, b = 1) { 
 while (n--) {
  [a, b] = [b, a + b]
 }
 return a
}

這種方案簡單粗暴,缺點就是沒有遞歸的那種寫法比較容易理解。

方案二:Trampolining(蹦床函數)

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

function f(n, a = 0, b = 1) { 
 if (n > 0) {
  [a, b] = [b, a + b]
  return f.bind(null, n - 1, a, b)
 } else {
  return a
 }
}

trampoline(f(5)) // return 5

這種寫法算是容易理解一些了,就是蹦床函數的作用需要仔細看看。缺點還有就是需要修改原函數內部的寫法。

方案三:尾遞歸函數轉循環方法

function tailCallOptimize(f) { 
 let value
 let active = false
 const accumulated = []
 return function accumulator() {
  accumulated.push(arguments)
  if (!active) {
   active = true
   while (accumulated.length) {
    value = f.apply(this, accumulated.shift())
   }
   active = false
   return value
  }
 }
}

const f = tailCallOptimize(function(n, a = 0, b = 1) { 
 if (n === 0) return a
 return f(n - 1, b, a + b)
})
f(5) // return 5

經過 tailCallOptimize 包裝后返回的是一個新函數 accumulator,執行 f時實際執行的是這個函數。這種方法可以不用修改原遞歸函數,當調用遞歸時只用使用該方法轉置一下便可解決遞歸調用的問題。

感謝各位的閱讀!關于“JavaScript調用棧、尾遞歸和手動優化的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女