經過以前的文章,咱們熟悉了做用域的基本概念。可是做用域中的變量,函數聲明在什麼地方查找,引用它們的時候又發生了什麼。正是咱們將要討論的內容。前端
在咱們的認知中JavaScript
代碼在執行的時候是由上到下一行一行執行的。但實際上並不徹底正確。例如:編程
a = 1;
var a;
console.log(a);
複製代碼
按照咱們以前的認知由上到下,最後a
輸出undefined
,由於var a
聲明在a = 1
後面,但最後輸出的結果是1
。數組
考慮另一段代碼:函數
console.log(a);
var a = 1;
複製代碼
鑑於上一個代碼片斷所表現的特色,可能認爲這個代碼片斷也會輸出1
,或者可能拋出異常錯誤。實際上輸出的是undefined
。ui
那麼究竟是聲明在前,仍是賦值在前?this
爲了弄明白這個問題,咱們須要再次回顧JavaScript
引擎,引擎會在解釋JavaScript
代碼以前首先對其進行編譯。編譯階段中的一個很重要的工做就是找到全部的聲明,並在合適的做用域中將它們關聯起來。spa
執行環境也能夠叫執行上下文,每當JavaScript
編譯器工做時,都會建立一個執行環境或者說進入一個執行上下文中。它們定義了變量或函數訪問其餘數據的權限,決定了它們各自的行爲。它們在邏輯上組成一個堆棧,堆棧底部永遠是全局環境,而頂部就是當前環境。設計
例如:咱們能夠定義執行環境是一個數組:3d
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
}
}
複製代碼
抽象變量對象VO
全局執行環境變量對象GlobalContextVO (VO === this === global)
函數執行環境變量對象FunctionContextVO (VO === AO, 而且添加了arguments)
全局對象是在進入任何執行環境以前就已經建立了的對象;這個對象只存在一份,它的屬性在程序中任何地方均可以訪問,全局對象的生命週期終止於程序退出那一刻。
global = {
Math: <...>, String: <...> ... ... window: global //引用自身 }; var a = 1; console.log(a); // 直接訪問,在VO(globalContext)裏找到:a console.log(window['a']); // 間接經過global訪問:global === VO(globalContext): a console.log(a === this.a); // true var b = 'b'; console.log(window[b]); // 間接經過動態屬性名稱訪問:b 複製代碼
在函數執行環境中,變量對象是不能直接訪問的,此時由活動對象(AO)代替變量對象。活動對象是在進入函數執行環境時被建立的,它經過函數的arguments屬性初始化。
VO(functionContext) === AO;
AO = {
arguments: <Args> }; // Arguments對象是活動對象,屬性以下: // callee — 指向當前函數的引用 // length — 真正傳遞的參數個數 function foo(a, b, c) { // 聲明的函數參數數量arguments (a, b, c) console.log(foo.length); // 3 // 真正傳進來的參數個數(only x, y) console.log(arguments.length); // 2 // 參數的callee是函數自身 console.log(arguments.callee === foo); // true // 參數共享 console.log(a === arguments[0]); // true console.log(a); // 10 arguments[0] = 20; console.log(a); // 20 a = 30; console.log(arguments[0]); // 30 // 不過,沒有傳進來的參數c,和參數的第3個索引值是不共享的 c = 40; console.log(arguments[2]); // undefined arguments[2] = 50; console.log(c); // 40 } foo(10, 20); 複製代碼
當代碼在一個執行環境中,引擎會建立變量對象的一個做用域鏈。做用域鏈的用途是保證對執行環境有權限的變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則活動對象(VO)做爲變量對象(即arguments
對象)。做用域鏈中的下一個變量對象來自包含環境,而再下一個變量對象則來自下一個包含環境,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。
做用域鏈與一個執行環境相關,變量對象的做用域鏈用於在標識符解析中變量查找。
函數執行環境的做用域鏈在函數調用時建立的,包含活動對象和這個函數內部的[[scope]]屬性,例如:
var a = 1;
function foo() {
var b = 2;
console.log(a + b);
}
foo(); // 3
複製代碼
函數建立時:
fooContext.AO = {
b: undefined // undefined – 進入上下文的時候是2 – 活動對象
};
複製代碼
函數foo
如何訪問到變量a
?理論上函數應該能訪問一個更高一層執行環境的變量對象。實際上它正是這樣,這種機制是經過函數內部的[[scope]]屬性來實現的。[[scope]]是全部父變量對象的層級鏈,處於當前函數執行環境之上,在函數建立時存於其中,直至函數銷燬。函數foo
的[[scope]]以下:
foo.[[Scope]] = [
globalContext.VO // === Global
];
複製代碼
函數調用時:
Scope = AO|VO + foo.[[Scope]]
複製代碼
進入foo
執行環境建立AO/VO以後,在執行環境中建立一個做用域鏈(Scope屬性)。這樣foo
函數就能訪問全局執行環境中的變量a
。
經過前面對於引擎的瞭解,變量提高是Javascript
中執行環境和變量對象的工做方式的一種認知。它處於代碼的編譯階段,JavaScript
僅提高聲明,而不提高初始化。
當咱們的代碼運行時,首先在執行環境的變量對象中聲明變量和函數,而後纔是代碼執行階段。當咱們看到var a = 1
時,實際上JavaScript
會將其當作兩部分:var = a
和a = 1
。
var a;
a = 1;
//全局環境的變量對象
globalContext: {
vo: {
a: 1
}
}
複製代碼
函數聲明會首先被提高,而後纔是變量,例如:
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
複製代碼