js 調用棧機制與ES6尾調用優化介紹

調用棧的英文名叫做Call Stack,大家或多或少是有聽過的,但是對於js調用棧的工作方式以及如何在工作中利用這一特性,大部分人可能沒有進行過更深入的研究,這塊內容可以說對我們前端來說就是所謂的基礎知識,咋一看好像用處並沒有很大,但掌握好這個知識點,就可以讓我們在以後可以走的更遠,走的更快!

博客前端積累文檔公衆號GitHub

目錄

  1. 數據結構:棧
  2. 調用棧是什麼?用來做什麼?
  3. 調用棧的運行機制
  4. 調用棧優化內存
  5. 調用棧debug大法

數據結構:棧

棧是一種遵從後進先出(LIFO)原則的有序集合,新元素都靠近棧頂,舊元素都接近棧底。

生活中的栗子,幫助一下理解:

餐廳裏面堆放的盤子(棧),一開始放的都在下面(先進),後面放的都在上面(後進),洗盤子的時候先從上面開始洗(先出)。

調用棧是什麼?用來做什麼?

  1. 調用棧是一種棧結構的數據,它是由調用偵組成的
  2. 調用棧記錄了函數的執行順序和函數內部變量等信息

調用棧的運行機制

機制

程序運行到一個函數,它就會將其添加到調用棧中,當從這個函數返回的時候,就會將這個函數從調用棧中刪掉。

看一下例子幫助理解:

// 調用棧中的執行步驟用數字表示
printSquare(5); // 1 添加
function printSquare(x) {
    var s = multiply(x, x); // 2 添加 => 3 運行完成,內部沒有再調用其他函數,刪掉
    console.log(s); // 4 添加 => 5 刪掉
    // 運行完成 刪掉printSquare
}
function multiply(x, y) {
    return x * y;
}

調用棧中的執行步驟如下(刪除multiply的步驟被省略了):

調用偵

每個進入到調用棧中的函數,都會分配到一個單獨的棧空間,稱爲“調用偵”。

在調用棧中每個“調用偵”都對應一個函數,最上方的調用幀稱爲“當前幀”,調用棧是由所有的調用偵形成的。

找到一張圖片,調用偵:

調用棧優化內存

調用棧的內存消耗

如上圖,函數的變量等信息會被調用偵保存起來,所以調用偵中的變量不會被垃圾收集器回收

當函數嵌套的層級比較深了,調用棧中的調用偵比較多的時候,這些信息對內存消耗是非常大的。

針對這種情況除了我們要儘量避免函數層級嵌套的比較深之外,ES6提供了“尾調用優化”來解決調用偵過多,引起的內存消耗過大的問題。

何謂尾調用

尾調用指的是:函數的最後一步是調用另一個函數

function f(x){
  return g(x); // 最後一步調用另一個函數並且使用return
}
function f(x){
  g(x); // 沒有return 不算尾調用 因爲不知道後面還有沒有操作
  // return undefined; // 隱式的return
}

尾調用優化優化了什麼?

尾調用用來刪除外層無用的調用偵,只保留內層函數的調用偵,來節省瀏覽器的內存。

下面這個例子調用棧中的調用偵一直只有一項,如果不使用尾調用的話會出現三個調用偵:

a() // 1 添加a到調用棧
function a(){
    return b(); // 在調用棧中刪除a 添加b
}
function b(){
    return c() // 刪除b 添加c
}

防止爆棧

瀏覽器對調用棧都有大小限制,在ES6之前遞歸比較深的話,很容易出現“爆棧”問題(stack overflow)。

現在可以使用“尾調用優化”來寫一個“尾遞歸”,只保存一個調用偵,來防止爆棧問題。

注意

  1. 只有不再用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀。
如果要使用外層函數的變量,可以通過參數的形式傳到內層函數中
function a(){
    var aa = 1;
    let b = val => aa + val // 使用了外層函數的參數aa
    return b(2) // 無法進行尾調用優化
}
  1. 尾調用優化只在嚴格模式下開啓,非嚴格模式是無效的。
  2. 如果環境不支持“尾調用優化”,代碼還可以正常運行,是無害的!

更多

關於尾遞歸以及更多尾調用優化的內容,推薦查閱ES6入門-阮一峯

調用棧debug大法

查看調用棧有什麼用

  1. 查看函數的調用順序是否跟預期一致,比如不同判斷調用不同函數。
  2. 快速定位問題/修改三方庫的代碼。

    當接手一個歷史項目,或者引用第三方庫出現問題的時候,可以先查看對應API的調用棧,找到其中涉及的關鍵函數,針對性的修復它。

    通過查看調用棧的形式,幫助我快速定位問題,修改三方庫的源碼。

如何查看調用棧

  1. 只查看調用棧:console.trace
a()
function a() {
    b();
}
function b() {
    c()
}
function c() {
    let aa = 1;
    console.trace()
}

如圖所示,點擊右側還能查看代碼位置:

  1. bugger打斷點形式,這也是我最喜歡的調試方式:

結語

本文主要講了這幾個方面的內容:

  1. 理解調用棧的運行機制,對代碼背後的一些執行機制也可以更加了解,幫助我們在百尺竿頭更進一步。
  2. 我們應該在日常的code中,有意識的使用ES6的“尾調用優化”,來減少調用棧的長度,節省客戶端內存。
  3. 利用調用棧,對第三方庫或者不熟悉的項目,可以更快速的定位問題,提高我們debug速度。

最後:之前寫過一篇關於垃圾回收機制與內存泄露的文章,感興趣的同學可以擴展一下。

如果這篇文章幫助到了你,歡迎點贊和關注,你的支持是對我最大的鼓勵!

博客前端積累文檔公衆號GitHub

以上2019/5/19

參考資料:

JS垃圾回收機制與常見內存泄露的解決方法

ES6入門-阮一峯

JavaScript 如何工作:對引擎、運行時、調用堆棧的概述

淺析javascript調用棧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章