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

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

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

目錄

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

數據結構:棧

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

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

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

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

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

調用棧的運行機制

機制git

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

看一下例子幫助理解:github

// 調用棧中的執行步驟用數字表示
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的步驟被省略了):web

調用偵segmentfault

每一個進入到調用棧中的函數,都會分配到一個單獨的棧空間,稱爲「調用偵」。瀏覽器

在調用棧中每一個「調用偵」都對應一個函數,最上方的調用幀稱爲「當前幀」,調用棧是由全部的調用偵造成的。

找到一張圖片,調用偵:

調用棧優化內存

調用棧的內存消耗

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

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

針對這種狀況除了咱們要儘可能避免函數層級嵌套的比較深以外,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. debugger打斷點形式,這也是我最喜歡的調試方式:

結語

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

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

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

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

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

以上2019/5/19

參考資料:

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

ES6入門-阮一峯

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

淺析javascript調用棧

相關文章
相關標籤/搜索