0%

JS 奇怪的部分:函數呼叫與執行堆

這一系列為 Anthony Alicea 的 「JavaScript 全攻略:克服 JS 的奇怪部分」課程筆記。
JavaScript 在呼叫函式的時候,到底發生了什麼事?是如何被執行的?
了解這些重不重要?應該很重要,就像一部車,如果不知道結構,要修理它恐怕很難吧?抱著這總心態,既然要學 JavaScript,那就好好瞭解它的運作原理吧。

我們先來想像一下,以下這段碼,在被編譯的時候,是怎麼進行的。

1
2
3
4
5
function b() {}
function a() {
b();
}
a();

當我們呼叫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
2
3
4
5
6
7
8
9
function a() {
b();
var c;
}
function b() {
var d;
}
a();
var d;

我們所看到下面被函數呼叫的var cvar d也都在記憶體中了。

他們的執行順序是,首先a()函數會被呼叫,因此a()會被放入執行堆,同時進入a()的執行環境。接著,它會變成目前執行的程式,而最後一行的 var da ()下面所以不會被呼叫。

因為 JavaScript 是同步且一次執行一行,現在執行的程式就是目前的執行環境,也就是執行堆最上面的那個。所以當a()在最上面時 它會進入函式逐行執行,在 a 函式中呼叫b()函數,b()會創造出自己的執行環境,然後進入執行堆最上面,再逐行執行它裡面的程式,等執行完最後一行之後,再跳回去繼續執行a()函數裡的其他程式。

為什麼? 因為當我們結束b()函數後,b()的執行環境就會離開執行堆,所以現在在最上面的是誰?是a()。然後繼續執行剛剛在b()後面的var c

a()執行完畢後 a()會離開執行堆,接著下一行還沒被執行的程式,也就是在全域執行環境中,最後的var d

這就是 JavaScript 執行的順序。每當函數被呼叫,一個新的執行環境就被創造給函數,同時,this 變數被創造給這個函數,而裡面的變數在創造階段就已經建立了,
然後程式碼會被逐行的執行。但每當函數被呼叫,即使是被自己呼叫,一個新的執行環境就會進入執行堆,然後當執行完畢後,離開執行堆。所以無論最上面是什麼,那個就是正在執行的程式。–>正在執行的永遠在執行堆的最上面。

逐行的、同步的這個觀念是非常重要,了解程式執行的逐一步驟,才會預防一些不知名的問題。

單字

invoke the function 或是 function invocation, 意思是執行這個函數