咱們老是會在程序中定義一些函數和變量,以後會使用這些函數和變量來構建咱們的系統。
然而,對於解釋器來講,它又是如何以及從哪裏找到這些數據的(函數,變量)?當引用一個對象的時候,在解釋器內部又發生了什麼? 咱們大多數人都知道,變量和執行上下文是密切相關的:程序員
var a = 10; // 全局上下文中的變量
(function () {
var b = 20; // 函數上下文中的局部變量
})();
alert(a); // 10
alert(b); // "b" is not defined
複製代碼
不只如此,許多程序員也都知道,ECMAScript標準中指出獨立的做用域只有經過「函數代碼」(可執行代碼類型中的一種)才能建立出來。比方說,與C/C++不一樣的是,在ECMAScript中for循環的代碼塊是沒法建立本地上下文的:瀏覽器
for (var k in {a: 1, b: 2}) {
alert(k);
}
alert(k); // 儘管循環已經結束,可是變量「k」仍然在做用域中
複製代碼
下面就來詳細介紹下,當聲明變量和函數的時候,究竟發生了什麼。bash
A variable object (in abbreviated form — VO) is a special object related with an execution context and which stores:ecmascript
VO={}
複製代碼
VO同時也是有一個執行上下文的屬性:
activeExecutionContext = {
VO: {
// 上下文中的數據 (變量聲明(var), 函數聲明(FD), 函數形參(function arguments))
}
};
複製代碼
對變量的間接引用(經過VO的屬性名)只容許發生在全局上下文中的變量對象上(全局對象自己就是變量對象,這部分會在後續做相應的介紹)。 對於其餘的上下文而言,是沒法直接引用VO的,由於VO是實現層的。
聲明新的變量和函數的過程其實就是在VO中建立新的和變量以及函數名對應的屬性和屬性值的過程。
ide
以下所示: 函數
var a = 10;
function test(x) {
var b = 20;
};
test(30);
複製代碼
上述代碼對應的變量對象則以下所示:
// 全局上下文中的變量對象
VO(globalContext) = {
a: 10,
test:
};
// 「test」函數上下文中的變量對象
VO(test functionContext) = {
x: 30,
b: 20
};
複製代碼
可是,在實現層(標準中定義的),變量對象只是一個抽象的概念。在實際執行上下文中,VO可能徹底不叫VO,而且初始的結構也可能徹底不一樣。
AbstractVO (generic behavior of the variable instantiation process)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, object and are added)
複製代碼
接下來對這塊內容進行詳細介紹
全局對象是一個在進入任何執行上下文前就建立出來的對象;此對象以單例形式存在;它的屬性在程序任何地方均可以直接訪問,其生命週期隨着程序的結束而終止。工具
全局對象在建立的時候,諸如Math,String,Date,parseInt等等屬性也會被初始化,同時,其中一些對象會指向全局對象自己--好比,DOM中,全局對象上的window屬性就指向了全局對象(可是,並不是全部的實現都是如此): post
global = {
Math: <...>,
String: <...>
...
...
window: global
};</...></...>
複製代碼
在引用全局對象的屬性時,前綴一般能夠省略,由於全局對象是不能經過名字直接訪問的。然而,經過全局對象上的this值,以及經過如DOM中的window對象這樣遞歸引用的方式均可以訪問到全局對象:
String(10); // 等同於 global.String(10);
// 帶前綴
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
複製代碼
回到全局上下文的變量對象上--這裏變量對象是全局對象自己:
VO(globalContext) === glbal;
複製代碼
準確地理解這個事實是很是必要的:正是因爲這個緣由,當在全局上下文中聲明一個變量時,能夠經過全局對象上的屬性來間接的引用該變量(比方說,當變量名提早未知的狀況下)
var a = new String('test');
alert(a); // directly, is found in VO(globalContext): "test"
alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"
複製代碼
活躍對象會在進入函數上下文的時候建立出來,初始化的時候會建立一個arguments屬性, 其值就是Arguments對象:測試
AO = {
arguments:
}
複製代碼
Arguments對象是活躍對象上的屬性,它包含了以下屬性:ui
以下所示:
function foo(x, y, z) {
// 定義的函數參數(x,y,z)的個數
alert(foo.length); // 3
// 實際傳遞的參數個數
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
// 然而,對於沒有傳遞的參數z,
// 相關的arguments對象的index-property是不共享的
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
複製代碼
上述例子,在當前的Google Chrome瀏覽器中有個bug——參數z和arguments[2]也是互相共享的。
對變量對象的修改和這兩個階段密切相關。
要注意的是,這兩個處理階段是通用的行爲,與上下文類型無關(不論是全局上下文仍是函數上下文都是一致的)。
看下面這個🌰:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
複製代碼
當以10位參數進入"test"函數上下文的時候,對應的AO以下所示:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d:
e: undefined
};
複製代碼
注意了,上面的AO並不包含函數"x"。這是由於這裏的"x"並非函數聲明而是函數表達式(FunctionExpression 簡稱FE), 函數表達式不會對VO形成影響。儘管函數"_e"也是函數表達式,然而,正如咱們看到的,因爲它被賦值給了變量"e",所以它能夠經過"e"來訪問到。關於函數聲明和函數表達式會在別的文章作具體介紹。 至此,處理上下文代碼的第一階段介紹完了,接下來介紹第二階段--執行代碼階段。
AO['c'] = 10;
AO['e'] = <指向函數表達式"_e">;
複製代碼
再次注意到,這裏函數表達式"_e"仍在內存中,這是由於它被保存在聲明的變量"e"中,而一樣是函數表達式的"x"卻不在AO/VO中:若是嘗試在定義前或者定義後調用"x"函數,這時會發生"x未定義"的錯誤。未保存的函數表達式只有在定義或者遞歸時才能調用。
以下是更加典型的例子:
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
複製代碼
上述例子中,爲什麼「x」打印出來是函數呢?爲什麼在聲明前就能夠訪問到?又爲什麼不是10或者20呢?緣由在於,根據規則——在進入上下文的時候,VO會被填充函數聲明; 同一階段,還有變量聲明「x」,可是,正如此前提到的,變量聲明是在函數聲明和函數形參以後,而且,變量聲明不會對已經存在的一樣名字的函數聲明和函數形參發生衝突, 所以,在進入上下文的階段,VO填充爲以下形式:
VO = {};
VO['x'] =
// 發現var x = 10;
// 若是函數「x」還未定義
// 則 "x" 爲undefined, 可是,在咱們的例子中
// 變量聲明並不會影響同名的函數值
VO['x'] =
複製代碼
隨後,在執行代碼階段,VO被修改成以下所示:
VO['x'] = 10;
VO['x'] = 20;
複製代碼
以下例子再次看到在進入上下文階段,變量存儲在VO中(所以,儘管else的代碼塊永遠都不會執行到,而「b」卻仍然在VO中):
if (true) {
var a = 1;
} else {
var b = 2;
}
alert(a); // 1
alert(b); // undefined, but not "b is not defined"
複製代碼
以下賦值語句:
a = 10;
複製代碼
僅僅是在全局對象上建立了新的屬性(而不是變量)。「不是變量」並不意味着它沒法改變,它是ECMAScript中變量的概念(它以後能夠變爲全局對象的屬性,由於VO(globalContext) === global,還記得吧?)
不一樣點以下所示:
alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;
複製代碼
接下來仍是要談到VO和在不一樣階段對VO的修改(進入上下文階段和執行代碼階段): 進入上下文:
VO = {
a: undefined
};
複製代碼
咱們看到,這個階段並無任何「b」,由於它不是變量,「b」在執行代碼階段纔出現。(可是,在咱們這個例子中也不會出現,由於在「b」出現前就發生了錯誤)
將上述代碼稍做改動:
alert(a); // undefined, we know why
b = 10;
alert(b); // 10, created at code execution 執行時建立
var a = 20;
alert(a); // 20, modified at code execution 修改時建立
複製代碼
這裏關於變量還有很是重要的一點:與簡單屬性不一樣的是,變量是不能刪除的{DontDelete},這意味着要想經過delete操做符來刪除一個變量是不可能的。
a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
alert(window.b); // 20
alert(delete b); // false
alert(window.b); // still 20
複製代碼
可是,這裏有個例外,就是「eval」執行上下文中,是能夠刪除變量的:
eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
複製代碼
利用某些debug工具,在終端測試過這些例子的童鞋要注意了:其中Firebug也是使用了eval來執行終端的代碼。所以,這個時候var也是能夠刪除的。
以下所示(SpiderMonkey,Rhino)
var global = this;
var a = 10;
function foo() {}
alert(foo.__parent__); // global
var VO = foo.__parent__;
alert(VO.a); // 10
alert(VO === global); // true
複製代碼
上述例子中,能夠看到函數foo是在全局上下文中建立的,相應的,它的__parent__屬性設置爲全局上下文的變量對象,好比說:全局對象。
然而,在SpiderMonkey中以相同的方式獲取活躍對象是不可能的:不一樣的版本表現都不一樣,內部函數的__parent__屬性會返回null或者全局對象。 在Rhino中,以相同的方式獲取活躍對象是容許的
以下所示(Rhino):
var global = this;
var x = 10;
(function foo() {
var y = 20;
// the activation object of the "foo" context
var AO = (function () {}).__parent__;
print(AO.y); // 20
// __parent__ of the current activation
// object is already the global object,
// i.e. the special chain of variable objects is formed,
// so-called, a scope chain
print(AO.__parent__ === global); // true
print(AO.__parent__.x); // 10
})();
複製代碼
重學JavaScript深刻理解系列(一)
重學JavaScript深刻理解系列(三)
重學JavaScript深刻理解系列(四)
重學JavaScript深刻理解系列(五)
重學JavaScript深刻理解系列(六)