調用棧的英文名叫作Call Stack,你們或多或少是有聽過的,可是對於js調用棧的工做方式以及如何在工做中利用這一特性,大部分人可能沒有進行過更深刻的研究,這塊內容能夠說對咱們前端來講就是所謂的基礎知識,咋一看好像用處並無很大,但掌握好這個知識點,就可讓咱們在之後能夠走的更遠,走的更快!javascript
博客、 前端積累文檔、 公衆號、 GitHub
棧是一種聽從後進先出(LIFO
)原則的有序集合,新元素都靠近棧頂,舊元素都接近棧底。html
生活中的栗子,幫助一下理解:前端
餐廳裏面堆放的盤子(棧),一開始放的都在下面(先進),後面放的都在上面(後進),洗盤子的時候先從上面開始洗(先出)。java
機制: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)。
如今可使用「尾調用優化」來寫一個「尾遞歸」,只保存一個調用偵,來防止爆棧問題。
注意:
若是要使用外層函數的變量,能夠經過參數的形式傳到內層函數中
function a(){ var aa = 1; let b = val => aa + val // 使用了外層函數的參數aa return b(2) // 沒法進行尾調用優化 }
更多:
關於尾遞歸以及更多尾調用優化的內容,推薦查閱ES6入門-阮一峯
查看調用棧有什麼用
當接手一個歷史項目,或者引用第三方庫出現問題的時候,能夠先查看對應API的調用棧,找到其中涉及的關鍵函數,針對性的修復它。
經過查看調用棧的形式,幫助我快速定位問題,修改三方庫的源碼。
如何查看調用棧
console.trace
a() function a() { b(); } function b() { c() } function c() { let aa = 1; console.trace() }
如圖所示,點擊右側還能查看代碼位置:
debugger
打斷點形式,這也是我最喜歡的調試方式:
本文主要講了這幾個方面的內容:
最後:以前寫過一篇關於垃圾回收機制與內存泄露的文章,感興趣的同窗能夠擴展一下。
以上2019/5/19
參考資料: