先有雞還是先有蛋
通過之前的文章,我們熟悉了作用域的基本概念。但是作用域中的變數,函式宣告在什麼地方查詢,引用它們的時候又發生了什麼。正是我們將要討論的內容。
在我們的認知中JavaScript
程式碼在執行的時候是由上到下一行一行執行的。但實際上並不完全正確。例如:
a = 1;
var a;
console.log(a);
複製程式碼
按照我們之前的認知由上到下,最後a
輸出undefined
,因為var a
宣告在a = 1
後面,但最後輸出的結果是1
。
考慮另外一段程式碼:
console.log(a);
var a = 1;
複製程式碼
鑑於上一個程式碼片段所表現的特點,可能認為這個程式碼片段也會輸出1
,或者可能丟擲異常錯誤。實際上輸出的是undefined
。
那麼到底是宣告在前,還是賦值在前?
回顧JavaScript引擎
為了弄明白這個問題,我們需要再次回顧JavaScript
引擎,引擎會在解釋JavaScript
程式碼之前首先對其進行編譯。編譯階段中的一個很重要的工作就是找到所有的宣告,並在合適的作用域中將它們關聯起來。
執行環境
執行環境也可以叫執行上下文,每當JavaScript
編譯器工作時,都會建立一個執行環境或者說進入一個執行上下文中。它們定義了變數或函式訪問其他資料的許可權,決定了它們各自的行為。它們在邏輯上組成一個堆疊,堆疊底部永遠是全域性環境,而頂部就是當前環境。
例如:我們可以定義執行環境是一個陣列:
stack = [];
複製程式碼
在初始化階段,stack
是這樣的:
stack = [
globalContext
];
複製程式碼
每次函式執行,進入function
的時候,這個堆疊都會被壓入。
function foo(){
return 'hello';
}
foo();
複製程式碼
那麼,stack
將會發生改變:
stack = [
<foo> functionContext
globalContext
];
複製程式碼
每次函式退出也就是執行到return
的時候,都會退出當前的執行環境,相應的stack
就會彈出,棧中的指標會移動位置。相關程式碼執行完畢後,stack
只會包含全域性環境,一直到整個程式結束。
變數物件
在進行JavaScript
程式設計是總避免不了宣告函式和變數,在每個執行環境中有一個變數物件,我們定義的所有變數和函式都儲存在這個物件中。
變數物件(VO)儲存一下內容:
函式宣告(function)
變數宣告(var)
我們可以用一個JavaScript
物件來表示一個變數物件例如:
VO = {};
複製程式碼
如前面所說執行環境中有一個變數物件,它是執行環境的一個屬性,例如:
context = {
VO = {};
}
複製程式碼
當我們宣告一個變數或一個函式的時候,例如:
var a = 1;
function foo() {
var b = 20;
};
test();
複製程式碼
對應的變數物件是:
//全域性環境的變數物件
globalContext: {
vo: {
a: 1,
foo: function
}
}
//foo函式環境的變數物件
fooContext: {
vo: {
b: 20
}
}
複製程式碼
函式宣告和變數宣告會被提升
現在終於到了本文的核心點了,當我們的程式碼執行時,首先在執行環境的變數物件中宣告變數和函式,然後才是程式碼執行階段。當我們看到var a = 1
時,實際上JavaScript
會將其看成兩部分:var = a
和a = 1
。
var a;
a = 1;
複製程式碼
函式優先
函式宣告會首先被提升,然後才是變數,例如:
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
複製程式碼
小結
無論作用域中的宣告出現在什麼地方,都會在程式碼被執行前首先進行處理。可以將這個過程想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。