在上篇文章中,我們講到了,當一段代碼被執行時,JavaScript引擎先會對其進行編譯,並創建執行上下文。但是並沒有明確說明到底什么樣的代碼才算符合規范
那么接下來我們就來明確下,哪些情況下代碼才算是“一段”代碼,才會在執行之前就進行編譯並創建執行上下文。一般說來,有這么三種情況
- 當JavaScript執行全局代碼的時候,會編譯全局代碼並創建全局執行上下文,而且在整個頁面的生存周期內,全局執行上下文只有一份。
- 當調用一個函數的時候,函數體內的代碼會被編譯,並創建函數執行上下文,一般情況下,函數執行結束之后,創建的函數執行上下文會被銷毀。
- 當使用eval函數的時候,eval的代碼也會被編譯,並創建執行上下文。
好了,又進一步理解了執行上下文,那本節我們就在這基礎之上繼續深入,一起聊聊調用棧。學習調用棧至少有以下三點好處:
- 可以幫助你了解JavaScript引擎背后的工作原理;
- 讓你有調試JavaScript代碼的能力;
- 幫助你搞定面試,因為面試過程中,調用棧也是出境率非常高的題目
比如你在寫JavaScript代碼的時候,有時候可能會遇到棧溢出的錯誤,如下圖所示:
那為什么會出現這種錯誤呢?這就涉及到了調用棧的內容。你應該知道JavaScript中有很多函數,經常會出現在一個函數中調用另外一個函數的情況,調用棧就是用來管理函數調用關系的一種數據結構。因此要講清楚調用棧,你還要先弄明白函數調用和棧結構
#什么是函數調用
函數調用就是運行一個函數,具體使用方式是使用函數名稱跟着一對小括號。下面我們看個簡單的示例代碼
var a = 2 function add(){ var b = 10 return a+b } add()
這段代碼很簡單,先是創建了一個add函數,接着在代碼的最下面又調用了該函數。
那么下面我們就利用這段簡單的代碼來解釋下函數調用的過程。
在執行到函數add()之前,JavaScript引擎會為上面這段代碼創建全局執行上下文,包含了聲明的函數和變量,你可以參考下圖:
從圖中可以看出,代碼中全局變量和函數都保存在全局上下文的變量環境中。
執行上下文准備好之后,便開始執行全局代碼,當執行到add這兒時,JavaScript判斷這是一個函數調用,那么將執行以下操作:
- 首先,從全局執行上下文中,取出add函數代碼。
- 其次,對add函數的這段代碼進行編譯,並創建該函數的執行上下文和可執行代碼。
- 最后,執行代碼,輸出結果。
完整流程你可以參考下圖:
就這樣,當執行到add函數的時候,我們就有了兩個執行上下文了——全局執行上下文和add函數的執行上下文。
也就是說在執行JavaScript時,可能會存在多個執行上下文,那么JavaScript引擎是如何管理這些執行上下文的呢?
答案是通過一種叫棧的數據結構來管理的。那什么是棧呢?它又是如何管理這些執行上下文呢?
#什么是棧
關於棧,你可以結合這么一個貼切的例子來理解,一條單車道的單行線,一端被堵住了,而另一端入口處沒有任何提示信息,堵住之后就只能后進去的車子先出來,這時這個堵住的單行線就可以被看作是一個棧容器,車子開進單行線的操作叫做入棧,車子倒出去的操作叫做出棧。
在車流量較大的場景中,就會發生反復的入棧、棧滿、出棧、空棧和再次入棧,一直循環。
所以,棧就是類似於一端被堵住的單行線,車子類似於棧中的元素,棧中的元素滿足后進先出的特點。你可以參看下圖:
#什么是JavaScript的調用棧
JavaScript引擎正是利用棧的這種結構來管理執行上下文的。在執行上下文創建好后,JavaScript引擎會將執行上下文壓入棧中,通常把這種用來管理執行上下文的棧稱為執行上下文棧,又稱調用棧。
為便於你更好地理解調用棧,下面我們再來看段稍微復雜點的示例代碼:
var a = 2 function add(b,c){ return b+c } function addAll(b,c){ var d = 10 result = add(b,c) return a+result+d } addAll(3,6)
在上面這段代碼中,你可以看到它是在addAll函數中調用了add函數,那在整個代碼的執行過程中,調用棧是怎么變化的呢?
下面我們就一步步地分析在代碼的執行過程中,調用棧的狀態變化情況。
第一步,創建全局上下文,並將其壓入棧底。如下圖所示
從圖中你也可以看出,變量a、函數add和addAll都保存到了全局上下文的變量環境對象中。
全局執行上下文壓入到調用棧后,JavaScript引擎便開始執行全局代碼了。首先會執行a=2的賦值操作,執行該語句會將全局上下文變量環境中a的值設置為2。設置后的全局上下文的狀態如下圖所示:
接下來,第二步是調用addAll函數。當調用該函數時,JavaScript引擎會編譯該函數,並為其創建一個執行上下文,最后還將該函數的執行上下文壓入棧中,如下圖所示:
addAll函數的執行上下文創建好之后,便進入了函數代碼的執行階段了,這里先執行的是d=10的賦值操作,執行語句會將addAll函數執行上下文中的d由undefined變成了10。
然后接着往下執行,第三步,當執行到add函數調用語句時,同樣會為其創建執行上下文,並將其壓入調用棧,如下圖所示:
當add函數返回時,該函數的執行上下文就會從棧頂彈出,並將result的值設置為add函數的返回值,也就是9。如下圖所示:
緊接着addAll執行最后一個相加操作后並返回,addAll的執行上下文也會從棧頂部彈出,此時調用棧中就只剩下全局上下文了。最終如下圖所示:
至此,整個JavaScript流程執行結束了。
好了,現在你應該知道了調用棧是JavaScript引擎追蹤函數執行的一個機制,當一次有多個函數被調用時,通過調用棧就能夠追蹤到哪個函數正在被執行以及各函數之間的調用關系。
#在開發中,如何利用好調用棧
鑒於調用棧的重要性和實用性,那么接下來我們就一起來看看在實際工作中,應該如何查看和利用好調用棧。
#1. 如何利用瀏覽器查看調用棧的信息
當你執行一段復雜的代碼時,你可能很難從代碼文件中分析其調用關系,這時候你可以在你想要查看的函數中加入斷點,然后當執行到該函數時,就可以查看該函數的調用棧了。
這么說可能有點抽象,這里我們拿上面的那段代碼做個演示,你可以打開“開發者工具”,點擊“Source”標簽,選擇JavaScript代碼的頁面,然后在第3行加上斷點,並刷新頁面。你可以看到執行到add函數時,執行流程就暫停了,這時可以通過右邊“call stack”來查看當前的調用棧的情況,如下圖:
從圖中可以看出,右邊的“call stack”下面顯示出來了函數的調用關系:棧的最底部是anonymous,也就是全局的函數入口;中間是addAll函數;頂部是add函數。這就清晰地反映了函數的調用關系,所以在分析復雜結構代碼,或者檢查Bug時,調用棧都是非常有用的。
除了通過斷點來查看調用棧,你還可以使用console.trace()
來輸出當前的函數調用關系,比如在示例代碼中的add函數里面加上了console.trace()
,你就可以看到控制台輸出的結果,如下圖:
#2. 棧溢出(Stack Overflow)
現在你知道了調用棧是一種用來管理執行上下文的數據結構,符合后進先出的規則。不過還有一點你要注意,調用棧是有大小的,當入棧的執行上下文超過一定數目,JavaScript引擎就會報錯,我們把這種錯誤叫做棧溢出。
特別是在你寫遞歸代碼的時候,就很容易出現棧溢出的情況。比如下面這段代碼:
function division(a,b){ return division(a,b) } console.log(division(1,2))
當執行時,就會拋出棧溢出錯誤,如下圖:
從上圖你可以看到,拋出的錯誤信息為:超過了最大棧調用大小(Maximum call stack size exceeded)。
那為什么會出現這個問題呢?這是因為當JavaScript引擎開始執行這段代碼時,它首先調用函數division,並創建執行上下文,壓入棧中;然而,這個函數是遞歸的,並且沒有任何終止條件,所以它會一直創建新的函數執行上下文,並反復將其壓入棧中,但棧是有容量限制的,超過最大數量后就會出現棧溢出的錯誤。
理解了棧溢出原因后,你就可以使用一些方法來避免或者解決棧溢出的問題,比如把遞歸調用的形式改造成其他形式,或者使用加入定時器的方法來把當前任務拆分為其他很多小任務。
#總結
好了,今天的內容就講到這里,下面來總結下今天的內容。
- 每調用一個函數,JavaScript引擎會為其創建執行上下文,並把該執行上下文壓入調用棧,然后JavaScript引擎開始執行函數代碼。
- 如果在一個函數A中調用了另外一個函數B,那么JavaScript引擎會為B函數創建執行上下文,並將B函數的執行上下文壓入棧頂。
- 當前函數執行完畢后,JavaScript引擎會將該函數的執行上下文彈出棧。
- 當分配的調用棧空間被占滿時,會引發“堆棧溢出”問題。
- 棧是一種非常重要的數據結構,不光應用在JavaScript語言中,其他的編程語言,如C/C++、Java、Python等語言,在執行過程中也都使用了棧來管理函數之間的調用關系。所以棧是非常基礎且重要的知識點,你必須得掌握。
#思考時間
最后,我給你留個思考題,你可以看下面這段代碼:
function runStack (n) {
if (n === 0) return 100;
return runStack( n- 2);
}
runStack(50000)
這是一段遞歸代碼,可以通過傳入參數n,讓代碼遞歸執行n次,也就意味着調用棧的深度能達到n,當輸入一個較大的數時,比如50000,就會出現棧溢出的問題,那么你能優化下這段代碼,以解決棧溢出的問題嗎?
// 優化 function runStack(n) { while (true) { if (n === 0) { return 100; } if (n === 1) { // 防止陷入死循環 return 200; } n = n - 2; } } console.log(runStack(50000));
from: http://blog.poetries.top/browser-working-principle/guide/part2/lesson08.html#%E6%80%9D%E8%80%83%E6%97%B6%E9%97%B4