前言
對于每個執(zhí)行上下文,都有三個重要屬性:
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
今天重點講講創(chuàng)建變量對象的過程。
變量對象
變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲了在上下文中定義的變量和函數(shù)聲明。
因為不同執(zhí)行上下文下的變量對象稍有不同,所以我們來聊聊全局上下文下的變量對象和函數(shù)上下文下的變量對象。
全局上下文
我們先了解一個概念,叫全局對象。在 W3School 中也有介紹:
全局對象是預定義的對象,作為 JavaScript 的全局函數(shù)和全局屬性的占位符。通過使用全局對象,可以訪問所有其他所有預定義的對象、函數(shù)和屬性。
在頂層 JavaScript 代碼中,可以用關(guān)鍵字 this 引用全局對象。因為全局對象是作用域鏈的頭,這意味著所有非限定性的變量和函數(shù)名都會作為該對象的屬性來查詢。
例如,當JavaScript 代碼引用 parseInt() 函數(shù)時,它引用的是全局對象的 parseInt 屬性。全局對象是作用域鏈的頭,還意味著在頂層 JavaScript 代碼中聲明的所有變量都將成為全局對象的屬性。
如果看的不是很懂的話,容我再來介紹下全局對象:
1.可以通過 this 引用,在客戶端 JavaScript 中,全局對象就是 Window 對象。
console.log(this);
2.全局對象是由 Object 構(gòu)造函數(shù)實例化的一個對象。
console.log(this instanceof Object);
3.預定義了一堆,嗯,一大堆函數(shù)和屬性。
// 都能生效
console.log(Math.random());
console.log(this.Math.random());
4.作為全局變量的宿主。
var a = 1;
console.log(this.a);
5.客戶端 JavaScript 中,全局對象有 window 屬性指向自身。
var a = 1;
console.log(window.a);
this.window.b = 2;
console.log(this.b);
花了一個大篇幅介紹全局對象,其實就想說:
全局上下文中的變量對象就是全局對象吶!
函數(shù)上下文
在函數(shù)上下文中,我們用活動對象(activation object, AO)來表示變量對象。
活動對象和變量對象其實是一個東西,只是變量對象是規(guī)范上的或者說是引擎實現(xiàn)上的,不可在 JavaScript 環(huán)境中訪問,只有到當進入一個執(zhí)行上下文中,這個執(zhí)行上下文的變量對象才會被激活,所以才叫 activation object 吶,而只有被激活的變量對象,也就是活動對象上的各種屬性才能被訪問。
活動對象是在進入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。
執(zhí)行過程
執(zhí)行上下文的代碼會分成兩個階段進行處理:分析和執(zhí)行,我們也可以叫做:
- 進入執(zhí)行上下文
- 代碼執(zhí)行
進入執(zhí)行上下文
當進入執(zhí)行上下文時,這時候還沒有執(zhí)行代碼,
變量對象會包括:
-
函數(shù)的所有形參 (如果是函數(shù)上下文)
- 由名稱和對應(yīng)值組成的一個變量對象的屬性被創(chuàng)建
- 沒有實參,屬性值設(shè)為 undefined
-
函數(shù)聲明
- 由名稱和對應(yīng)值(函數(shù)對象(function-object))組成一個變量對象的屬性被創(chuàng)建
- 如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性
-
變量聲明
- 由名稱和對應(yīng)值(undefined)組成一個變量對象的屬性被創(chuàng)建;
- 如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性
舉個例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在進入執(zhí)行上下文后,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
代碼執(zhí)行
在代碼執(zhí)行階段,會順序執(zhí)行代碼,根據(jù)代碼,修改變量對象的值
還是上面的例子,當代碼執(zhí)行完后,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
到這里變量對象的創(chuàng)建過程就介紹完了,讓我們簡潔的總結(jié)我們上述所說:
全局上下文的變量對象初始化是全局對象
函數(shù)上下文的變量對象初始化只包括 Arguments 對象
在進入執(zhí)行上下文時會給變量對象添加形參、函數(shù)聲明、變量聲明等初始的屬性值
在代碼執(zhí)行階段,會再次修改變量對象的屬性值
思考題
最后讓我們看幾個例子:
1.第一題
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
第一段會報錯:Uncaught ReferenceError: a is not defined。
第二段會打?。?code>1。
這是因為函數(shù)中的 "a" 并沒有通過 var 關(guān)鍵字聲明,所有不會被存放在 AO 中。
第一段執(zhí)行 console 的時候, AO 的值是:
AO = {
arguments: {
length: 0
}
}
沒有 a 的值,然后就會到全局去找,全局也沒有,所以會報錯。
當?shù)诙螆?zhí)行 console 的時候,全局對象已經(jīng)被賦予了 a 屬性,這時候就可以從全局找到 a 的值,所以會打印 1。
2.第二題
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
會打印函數(shù),而不是 undefined 。
這是因為在進入執(zhí)行上下文時,首先會處理函數(shù)聲明,其次會處理變量聲明,如果如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性。