深入JavaScript 執行上下文(三):變數物件
變數物件
變數物件是與執行上下文相關的資料作用域,儲存了在上下文中定義的變數和函式宣告。因為不同執行上下文下的變數物件稍有不同,所以我們來聊聊全域性上下文下的變數物件和函式上下文下的變數物件。
- 變數物件 / 活動物件:每個執行環境都有一個變數物件,環境中定義的所有變數和函式都儲存在這個物件中。如果這個環境是函式,則將其 活動物件 作為變數物件。
1、全域性上下文
我們先了解一個概念,叫全域性物件。在 W3School 中也有介紹:
(1)全域性物件是預定義的物件,作為 JavaScript 的全域性函式和全域性屬性的佔位符。通過使用全域性物件,可以訪問所有其他所有預定義的物件、函式和屬性。 (2)在頂層 JavaScript 程式碼中,可以用關鍵字 this 引用全域性物件。因為全域性物件是作用域鏈的頭,這意味著所有非限定性的變數和函式名都會作為該物件的屬性來查詢。
其實 全域性物件 就是 全域性上下文中的 變數物件
~~
// (1)可以通過 this 引用,在客戶端 JavaScript 中,全域性物件就是 Window 物件。 console.log(this); //window //(2)全域性物件是由 Object 建構函式例項化的一個物件。 console.log(this instanceof Object); //true //(3)作為全域性變數的宿主。 var a = 1; console.log(this.a); //1 //(4)客戶端 JavaScript 中,全域性物件有 window 屬性指向自身。 var a = 1; console.log(window.a); //1 this.window.b = 2; console.log(this.b); //2
2、函式上下文
在函式上下文中,我們用活動物件(activation object, AO)來表示變數物件。
活動物件和變數物件其實是一個東西,只是變數物件是規範上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有當 變數物件 進入一個函式執行上下文中,這個執行上下文的變數物件才會被啟用,所以才叫 activation object
,而只有被啟用的變數物件
,也就是活動物件
上的各種屬性才能被訪問。
活動物件是在進入函式上下文時刻被建立的,它通過函式的 arguments 屬性初始化
。arguments 屬性值是 Arguments 物件。
3、執行過程
執行上下文的程式碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:
- (1)進入執行上下文
- (2)程式碼執行
(1)進入執行上下文
當進入執行上下文時,這時候還沒有執行程式碼,
變數物件會包括:(處理順序由上到下)
- 1、函式的所有形參 (如果是函式上下文)
- 由名稱和對應值組成的一個變數物件的屬性被建立
- 沒有實參,屬性值設為 undefined
- 2、函式宣告
- 由名稱和對應值(函式物件(function-object))組成一個變數物件的屬性被建立
- 如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性
- 3、變數宣告
- 由名稱和對應值(undefined)組成一個變數物件的屬性被建立;
- 如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性
舉個例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在進入執行上下文後,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
(2)程式碼執行
在程式碼執行階段,會順序執行程式碼
,根據程式碼,修改變數物件的值
。
還是上面的例子,當代碼執行完後,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
總結上述: 1、 全域性上下文的變數物件初始化是全域性物件 2、函式上下文的變數物件初始化只包括 Arguments 物件 3、在進入執行上下文時會給變數物件新增形參、函式宣告、變數宣告等初始的屬性值 4、在程式碼執行階段,會再次修改變數物件的屬性值
4、思考題
例1:
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
第一段會報錯:Uncaught ReferenceError: a is not defined。 第二段會列印:1。
這是因為函式中的 “a” 並沒有通過 var 關鍵字宣告,所有不會被存放在 AO 中。 第一段執行 console 的時候, AO 的值是:
AO = {
arguments: {
length: 0
}
}
沒有 a 的值,然後就會到全域性去找,全域性也沒有,所以會報錯。
當第二段執行 console 的時候,全域性物件已經被賦予了 a 屬性,這時候就可以從全域性找到 a 的值,所以會列印 1。
例2:
console.log(foo); //???
function foo(){
console.log("foo");
}
var foo = 1;
列印: ƒ foo(){console.log("foo");}
,而不是 undefined 。
在進入執行上下文時,首先
會處理函式宣告
,其次
會處理變數宣告
,如果如果變數名稱
跟已經宣告的形式引數或函式相同
,則變數宣告不會干擾已經存在的這類屬性
。
例2.1
var foo = 1;
console.log(foo); //???
function foo(){
console.log("foo");
};
列印: 1
有些人會問:為什麼不列印foo函式呢?是例2說法錯了嗎? 並不是!其實我們可以把上面程式碼分解成如下:
function foo(){
console.log("foo");
};
var foo; // 如果如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性
//到此,VO為:VO = { foo: reference to function foo(){}}
foo = 1; // 程式碼執行。PS: 如果沒有這行,列印結果是 function foo()
//到此,VO為:VO = { foo: 1}
console.log(foo); // 1
例2.2
console.log(foo); //???
var foo = 1;
console.log(foo); //???
function foo(){};
第一行列印:ƒ foo(){console.log(“foo”);} 第二行列印:1
可以這麼理解:
foo() //函式提升
var foo //和函式重名了,被忽略
console.log(foo); //列印函式
foo = 1; //全域性變數foo
console.log(foo); //列印1,事實上函式foo已經不存在了,變成了1