JavaScript不同於其他語言,存在變數提升,如下面程式碼例子:
console.log(x)
var x = 'hello world';
複製程式碼
這段程式碼不會報錯,會輸出 undefined
。這就是所謂的變數提升,但具體細節JS引擎是怎麼處理的,還需要理解JS的Execution Context執行上下文。
1. Execution Context
Execution Context 是JS執行程式碼時候的一個上下文環境。如執行到一個呼叫函式,就會進入這個函式的執行上下文,執行上下文中會確定這個函式執行期間用到的諸如this,變數,物件以及定義的方法等。
當瀏覽器載入script的時候,預設直接進入Global Execution Context(全域性上下文),將全域性上下文入棧。如果在程式碼中呼叫了函式,則會建立Function Execution Context(函式上下文)並壓入呼叫棧內,變成當前的執行環境上下文。當執行完該函式,該函式的執行上下文便從呼叫棧彈出返回到上一個執行上下文。
2. 執行上下文分類
-
Global execution context。當js檔案載入進瀏覽器執行的時候,進入的就是全域性執行上下文。全域性變數都是在這個執行上下文中。程式碼在任何位置都能訪問。
-
Functional execution context。定義在具體某個方法中的上下文。只有在該方法和該方法中的內部方法中訪問。
-
Eval。定義在Eval方法中的上下文。該方法不建議使用對此就不進一步研究。
3. Execution Stack
Js是單執行緒執行,每次註定只能訪問一個execution context。因此呼叫棧最上方的執行上下文將最先被執行,執行完後返回到上層的執行上下文繼續執行。引用一篇博文的動態圖示如下:
4. 執行上下文執行詳情
execution context期間js引擎主要分兩個階段:
建立階段(函式呼叫時,但在函式執行前)
-
JS解析器掃描一遍程式碼,建立execution context內對應的variables, functions和arguments。這三個稱之為Variable Object。
-
建立作用域鏈scope chain
-
決定this的指向
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
'this': {}
}
複製程式碼
executionContextObj由函式呼叫時執行前建立,建立階段arguments的引數會直接傳入,函式內部定義的變數會初始化為undefined。
執行階段
- 重新掃描一次程式碼,給變數賦值,然後執行程式碼。
下面是執行上下文期間JS引擎執行虛擬碼
- 找到呼叫函式
- 執行函式程式碼前,建立execution context
- 進行建立階段:
- 初始化呼叫鏈 Scope Chain
- 建立 variable object:
- 建立arguments物件,初始化該入參變數名和值
- 掃描該執行上下文中宣告的函式:
- 對於宣告的函式,variable object中建立對應的變數名,其值指向該函式(函式是存在heap中的)
- 如果函式名已經存在,用新的引用值覆蓋已有的
- 掃描上下文中宣告的變數:
- 對於變數的宣告,同樣在variable object中建立對應的變數名,其值初始化為undefined
- 如果變數的名字已經存在,則直接略過繼續掃描
- 決定上下文this的指向
- 程式碼執行階段:
- 執行函式內的程式碼並給對應變數進行賦值(建立階段為undefined的變數)
一個簡單例子如下:
console.log(foo(22))
console.log(x);
var x = 'hello world';
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
console.log(i)
}
複製程式碼
(a):程式碼首先進入到全域性上下文的建立階段。
ExecutionContextGlobal = {
scopeChain: {...},
variableObject: {
x: undefined,
foo: pointer to function foo()
},
this: {...}
}
複製程式碼
然後進入全域性執行上下文的執行階段。這一階段從上至下逐條執行程式碼,執行到console.log(foo(22))
該行時,建立階段已經為variableObject中的foo賦值了,因此執行時會執行foo(22)
函式。
當執行foo(22)
函式時,又將進入foo()
的執行上下文,詳見(b)。
當執行到console.log(x)
時,此時x
在variableObject中賦值為undefined
,因此列印出undefined
,這也正是變數提升產生的結果。
當執行到var x = 'hello world';
,variableObject中的x被賦值為hello world
。
繼續往下是foo
函式的宣告,因此什麼也不做,執行階段結束。下面是執行階段完成後的ExecutionContextGlobal。
ExecutionContextGlobal = {
scopeChain: {...},
variableObject: {
x: 'hello world',
foo: pointer to function foo()
},
this: {...}
}
複製程式碼
(b):當js呼叫foo(22)時,進入到foo()函式的執行上下文,首先進行該上下文的建立階段。
ExecutionContextFoo = {
scopeChain: {...},
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: {...}
}
複製程式碼
當執行階段執行完後,ExecutionContextFoo如下。
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
複製程式碼
理清了JS中的執行上下文,就很容易明白變數提升具體是怎麼回事了。在程式碼執行前,執行上下文已經給對應的宣告賦值,只不過變數是賦值為undefined
,函式賦值為對應的引用,而後在執行階段再將對應值賦值給變數。
5. 區分函式宣告和函式表示式
首先看下面幾個程式碼片段,分別輸出是什麼?
Question 1:
function foo(){
function bar() {
return 3;
}
return bar();
function bar() {
return 8;
}
}
alert(foo());
複製程式碼
Question 2:
function foo(){
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
alert(foo());
複製程式碼
Question 3:
alert(foo());
function foo(){
var bar = function() {
return 3;
};
return bar();
var bar = function() {
return 8;
};
}
複製程式碼
Question 4:
function foo(){
return bar();
var bar = function() {
return 3;
};
var bar = function() {
return 8;
};
}
alert(foo());
複製程式碼
上面4個程式碼片段分別輸出 8
,3
,3
,[Type Error: bar is not a function]
。
function name([param,[, param,[..., param]]]) { [statements] }
函式宣告以關鍵字function
開頭定義函式,同時有確定的函式名。如最簡單的栗子:
function bar() {
return 3;
}
複製程式碼
通過函式執行上下文,函式宣告會產生hoisted,即函式宣告會提升到程式碼最上面。
所以在Question 1中,foo.VO中 bar:pointer to the function bar()
,因為有宣告瞭兩次bar()
函式,所以後面的定義覆蓋前面的定義。
var myFunction = function [name]([param1[, param2[, ..., paramN]]]) { statements };
函式表示式中,函式名字可以省略,簡單栗子如下:
//anonymous function expression
var a = function() {
return 3;
}
//named function expression
var a = function bar() {
return 3;
}
//self invoking function expression
(function sayHello() {
alert("hello!");
})();
複製程式碼
以上三種都是函式表示式,最後一種是立即執行函式。函式表示式不會提升到程式碼最上面,如Question 2中,在函式執行上下文的建立階段中,foo.VO 中 bar : undefined
,在執行階段才進行賦值。
在回頭看看Question 4:
function foo(){
return bar(); // 執行階段返回撥用bar(),但建立階段bar被賦值為 undefined,所以報Type Error。
var bar = function() {
return 3;
};
var bar = function() {
return 8;
};
}
alert(foo());
複製程式碼
參考
What is the Execution Context & Stack in JavaScript?