這一系列為 Anthony Alicea 的 「JavaScript 全攻略:克服 JS 的奇怪部分」課程筆記。
JavaScript 在呼叫函式的時候,到底發生了什麼事?是如何被執行的?
了解這些重不重要?應該很重要,就像一部車,如果不知道結構,要修理它恐怕很難吧?抱著這總心態,既然要學 JavaScript,那就好好瞭解它的運作原理吧。
我們先來想像一下,以下這段碼,在被編譯的時候,是怎麼進行的。
1 | function b() {} |
當我們呼叫a()
函數時,第一個被創造的是全域執行環境(Global Execution)。語法解析器會分析程式,然後編譯程式,接著創造全域執行環境與全域變數 this
。如果是在瀏覽器,就會創造windows
這個物件,然後將這些函數放進記憶體中。這裡的this
等同全域物件或瀏覽器裡的 window。
在創造的第一個階段,是把a()
和b()
都先放在記憶體裡,然後程式碼會逐行被執行。雖然是先放在記憶體裡,但 JS 不會執行函數中的程式,因為它們還沒被呼叫。
當編譯器遇到最下面這個a()
時,a()
函數被呼叫了,這時候a()
會被放進執行堆(execution stack)中。這個執行堆會一個一個疊起來,誰在最上面的就是正在執行的東西。
所以每此 JavaScript 呼叫函數,就會創造一個新的執行環境,然後被放進執行堆中,一個新的執行環境被創造,就像全域執行環境一樣,會有它自己的記憶體空間給變數和函數。
這個過程會歷經創造階段,然後逐行執行函數中的程式。但是一但我們呼叫另一個函數,它就會停止執行程式,然後,再創造另一個執行環境,然後繼續執行。
如果函式裡沒有程式需要被執行,如b()
,JavaScript 仍會創造一個執行環境,然後如上述般,逐行執行程式。
當b()
結束後,因為它在執行堆的最上面,所以它會離開執行堆,然後是a()
,最後回到最下面的全域執行環境。
程式執行中實際排列的順序並不重要,在函數中剩下的程式碼順序也不會影響執行先後
,假設我們把a()
放到b()
前面,然後在函數裡多了一些程式(如下),這並不會影響執行,因為雖然a()
看起來在b()
前面,但這些函數在創造階段就已經在記憶體中了,也就是在全域執行環境被初始化時就在了。
1 | function a() { |
我們所看到下面被函數呼叫的var c
和var d
也都在記憶體中了。
他們的執行順序是,首先a()
函數會被呼叫,因此a()
會被放入執行堆,同時進入a()
的執行環境。接著,它會變成目前執行的程式,而最後一行的 var d
在 a ()
下面所以不會被呼叫。
因為 JavaScript 是同步且一次執行一行,現在執行的程式就是目前的執行環境,也就是執行堆最上面的那個。所以當a()
在最上面時 它會進入函式逐行執行,在 a 函式中呼叫b()
函數,b()
會創造出自己的執行環境,然後進入執行堆最上面,再逐行執行它裡面的程式,等執行完最後一行之後,再跳回去繼續執行a()
函數裡的其他程式。
為什麼? 因為當我們結束b()
函數後,b()
的執行環境就會離開執行堆,所以現在在最上面的是誰?是a()
。然後繼續執行剛剛在b()
後面的var c
當a()
執行完畢後 a()會離開執行堆,接著下一行還沒被執行的程式,也就是在全域執行環境中,最後的var d
這就是 JavaScript 執行的順序。每當函數被呼叫,一個新的執行環境就被創造給函數,同時,this
變數被創造給這個函數,而裡面的變數在創造階段就已經建立了,
然後程式碼會被逐行的執行。但每當函數被呼叫,即使是被自己呼叫,一個新的執行環境就會進入執行堆,然後當執行完畢後,離開執行堆。所以無論最上面是什麼,那個就是正在執行的程式。–>正在執行的永遠在執行堆的最上面。
逐行的、同步的這個觀念是非常重要,了解程式執行的逐一步驟,才會預防一些不知名的問題。
單字
invoke the function 或是 function invocation, 意思是執行這個函數