從ECMAScript規範深度分析JavaScript(二):變數物件(上)
本文譯自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中會加入一些個人見解以及配圖舉例等等,來幫助讀者更好的理解JavaScript。
宣告:本文不涉及與ES6相關的知識。
前言
在學習變數物件之前,我們要對執行期上下文有所瞭解,可以先看過《從ECMAScript規範深度分析JavaScript(一):執行期上下文》一文再來進行對變數物件的學習。
在程式中,我們會宣告一些函式和變數來幫助我們成功構建系統,但是直譯器是如何並且在哪裡找到我們的資料(函式,變數)的呢?當我們引用所需的物件時,發生了什麼呢?
許多ECMAScript程式設計師都知道變數物件和執行期上下文(execution context)密切相關:
var a = 10; // 全域性上下文(global context)的全域性變數
(function () {
var b = 20; // 函式上下文function context對的區域性變數
})();
alert(a); // 10
alert(b); // "b" is not defined
同時,很多程式設計師知道,在當前版本規範中獨立作用域只能由函式程式碼型別的執行期上下文建立。比如,以ECMAScript中的for迴圈塊為例,不同於C/C++,它不會建立一個區域性上下文(即區域性作用域):
for (var k in {a: 1, b: 2}) { alert(k); }
alert(k); //當迴圈結束後,k變數依然存在於作用域中。
讓我們一起來深入瞭解一下當我們宣告我們的資料時,到底發生了什麼事情。
資料宣告
如果變數和執行期上下文相關,那麼他就應該知道他的資料在哪兒儲存以及如何在哪兒獲取。這個機制叫做變數物件(variable object)。
一個變數物件(variable object,簡稱VO)是一個與上下文相關的特殊物件(注:這個物件在js不可獲取,只是一個實現上的概念),儲存了在上下文中宣告的以下內容:
- 變數宣告
- 函式宣告
- 函式的形參
在ES5中變數物件(VO)的概念被詞法環境(lexical environment)模型所替代了,理解了變數物件(VO)的概念,後面的新概念也很容易就理解了。
簡單舉個例子,我們完全可以將變數物件表示為一個普通ECMAScript物件:
VO = {};
如我們所說,變數物件VO是一個執行期上下文的一個屬性:
activeExecutionContext = {
VO: {
// 上下文資料 (變數,函式,形參)
}
};
間接的引用變數(通過VO的屬性名)只在全域性上下文中被允許(因為在全域性上下文裡,全域性物件自身就是變數物件)。在其它上下文中是不可能直接訪問到VO的,因為變數物件僅僅是實現機制。
當我們聲明瞭一個變數或者函式,除了使用它們的名稱和值在VO中創了一個新屬性外,其他什麼事也沒做。
比如:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
對應的變數物件為:
// 全域性上下文的變數物件
VO(globalContext) = {
a: 10,
test: <reference to function>
};
// test函式上下文的變數物件
VO(test functionContext) = {
x: 30,
b: 20
};
但是我們要注意:在實現層面和規範上,變數物件只是一個抽象的事物。從本質上講,在具體的執行期上下文中,VO的名稱不同並且有不同的初始結構。
不同執行期上下文中的變數物件
在所有型別的執行期上下文中,變數物件的一些操作(比如變數宣告)和行為都是一致的。從這個觀點很容易將變數物件作為一個抽象的基本事物來描述。函式上下文可以對變數物件定義一些額外的細節
AbstractVO (變數初始化的通用行為)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, 包含額外的arguments物件和形參)
我們來對此進行深入討論
1、全域性上下文中的變數物件
在這裡,首先有必要要給出全域性物件(Global object)的概念:
全域性物件是在進入執行期上下文之前進行建立,只存在一個全域性物件,全域性物件的屬性在程式的任何地方都能夠訪問到,全域性物件的宣告週期在程式結束時才結束。
在全域性物件建立的時候,會初始化一些比如Math,String,Date,parseInt等屬性,同樣也有一些額外指向全域性物件自身的屬性——比如在BOM中,全域性物件的window屬性指向全域性物件(注意,不是在所有實現中都是這樣):
global = {
Math: <...>,
String: <...>
...
...
window: global
};
因為全域性物件沒法通過名稱來直接訪問,所以當引用全域性物件的屬性時通常會省略字首。但是,我們可以通過全域性物件的this值來訪問,也可以通過引用自身的屬性來訪問,比如BOM中的window,所以我們簡寫為:
String(10); // global.String(10);
// 有字首
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
所以,說回到全域性上下文的變數物件,就是全域性物件本身:
VO(globalContext) === global;
正確立即這個概念是很有必要的,因為由於這個原因,在全域性上下文中宣告一個變數,我們可以通過全域性物件的屬性名間接的引用它(在我們沒法提前知道這個屬性名的時候這將會很有用):
var a = new String('test');
alert(a); // 直接引用, 會在VO(globalContext)中找到
alert(window['a']); // 通過 global === VO(globalContext間接引用
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // 當屬性名為動態不確定時,間接引用
2、函式上下文的變數物件
我們知道,在函式執行上下文中,VO是不能直接訪問的,此時由啟用物件(activation object,簡稱為AO)扮演VO的角色。
VO(functionContext) === AO;
啟用物件在進入函式上下文時被建立,並且由Arguments物件作為arguments屬性進行初始化:
AO = {
arguments: <ArgO>
};
Argunemts物件時啟用物件的一個屬性,包含以下屬性:
- callee:當前函式的引用;
- length:實際傳參的數量;
- properties-indexes(integer會被轉為string)屬性下標:該屬性的值就是函式的引數值(按引數列表從左到右排列),內部元素個數等於arguments.length,arguments物件的properties-indexes值與實際形參引數是共享的(因為引用地址相同)。
例如:
function foo(x, y, z) {
// 函式定義形參的個數 (x, y, z)
alert(foo.length); // 3
// 實際傳參的數量 (只有 x, y)
alert(arguments.length); // 2
// 指向函式自身
alert(arguments.callee === foo); // true
// 引數共享
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// however, for not passed argument z,
// related index-property of the arguments
// object is not shared
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
最後一個例子中的場景,在某些瀏覽器中有bug,就是即使沒有傳遞引數z,z和arguments[2]仍然是共享的(其實也不能說是bug,因為以前的處理機制中它們倆的引用地址就是同一個,所以改變arguments[2]的值,z當然也會跟著改變)。
注:在ES5中啟用物件的概念也被公共的和單一模式的詞法環境的概念所取代。
結語
本文討論了資料宣告是變數物件的變化,以及在不同執行期上下文中變數物件的區別和在某些瀏覽器中需要注意的點。
希望此文能夠解決大家工作和學習中的一些疑問,避免不必要的時間浪費,有不嚴謹的地方,也請大家批評指正,共同進步!
轉載請註明出處,謝謝!
交流方式:QQ1670765991