深入理解JavaScript系列(12):變數對象(Variable Object)

來源:互聯網
上載者:User
介紹

JavaScript編程的時候總避免不了聲明函數和變數,以成功構建我們的系統,但是解譯器是如何並且在什麼地方去尋找這些函數和變數呢?我們引用這些對象的時候究竟發生了什嗎?

原始發布:Dmitry A. Soshnikov
發布時間:2009-06-27
俄文地址:http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/

英文翻譯:Dmitry A. Soshnikov
發布時間:2010-03-15
英文地址:http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/

部分難以翻譯的句子參考了justinw的中文翻譯

大多數ECMAScript程式員應該都知道變數與執行內容有密切關係:

var a = 10; // 全域上下文中的變數

(function () {
var b = 20; // function上下文中的局部變數
})();

alert(a); // 10
alert(b); // 全域變數 "b" 沒有聲明

並且,很多程式員也都知道,當前ECMAScript規範指出獨立範圍只能通過“函數(function)”代碼類型的執行內容建立。也就是說,相對於C/C++來說,ECMAScript裡的for迴圈並不能建立一個局部的上下文。

for (var k in {a: 1, b: 2}) {
alert(k);
}

alert(k); // 儘管迴圈已經結束但變數k依然在當前範圍

我們來看看一下,我們聲明資料的時候到底都發現了什麼細節。

資料聲明

如果變數與執行內容相關,那變數自己應該知道它的資料存放區在哪裡,並且知道如何訪問。這種機制稱為變數對象(variable object)。

變數對象(縮寫為VO)是一個與執行內容相關的特殊對象,它儲存著在上下文中聲明的以下內容:
變數 (var, 變數聲明);
函式宣告 (FunctionDeclaration, 縮寫為FD);
函數的形參

舉例來說,我們可以用普通的ECMAScript對象來表示一個變數對象:

VO = {};

就像我們所說的, VO就是執行內容的屬性(property):

activeExecutionContext = {
VO: {
// 上下文資料(var, FD, function arguments)
}
};

只有全域內容相關的變數對象允許通過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名稱是不一樣的,並且初始結構也不一樣。

不同執行內容中的變數對象

對於所有類型的執行內容來說,變數對象的一些操作(如變數初始化)和行為都是共通的。從這個角度來看,把變數對象作為抽象的基本事物來理解更為容易。同樣在函數上下文中也定義和變數對象相關的額外內容。

抽象變數對象VO (變數初始化過程的一般行為)

╠══> 全域上下文變數對象GlobalContextVO
║ (VO === this === global)

╚══> 函數上下文變數對象FunctionContextVO
(VO === AO, 並且添加了<arguments>和<formal parameters>)

我們來詳細看一下:

全域上下文中的變數對象

首先,我們要給全域對象一個明確的定義:

全域對象(Global object) 是在進入任何執行內容之前就已經建立了的對象;
這個對象只存在一份,它的屬性在程式中任何地方都可以訪問,全域對象的生命週期終止於程式退出那一刻。

全域對象初始建立階段將Math、String、Date、parseInt作為自身屬性,等屬性初始化,同樣也可以有額外建立的其它對象作為屬性 (其可以指向到全域對象自身)。例如,在DOM中,全域對象的window屬性就可以引用全域對象自身(當然,並不是所有的具體實現都是這樣):

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) === global;

非常有必要要理解上述結論,基於這個原理,在全域上下文中聲明的對應,我們才可以間接通過全域對象的屬性來訪問它(例如,事先不知道變數名稱)。

var a = new String('test');

alert(a); // 直接存取,在VO(globalContext)裡找到:"test"

alert(window['a']); // 間接通過global訪問:global === VO(globalContext): "test"
alert(a === this.a); // true

var aKey = 'a';
alert(window[aKey]); // 間接通過動態屬性名稱訪問:"test"

函數上下文中的變數對象

在函數執行內容中,VO是不能直接存取的,此時由使用中的物件(activation object,縮寫為AO)扮演VO的角色。

VO(functionContext) === AO;

使用中的物件是在進入函數上下文時刻被建立的,它通過函數的arguments屬性初始化。arguments屬性的值是Arguments對象:

AO = {
arguments: <ArgO>
};

Arguments對象是使用中的物件的一個屬性,它包括如下屬性:

  1. callee — 指向當前函數的引用
  2. length — 真正傳遞的參數個數
  3. properties-indexes (字串類型的整數) 屬性的值就是函數的參數值(按參數列表從左至右排列)。 properties-indexes內部元素的個數等於arguments.length. properties-indexes 的值和實際傳遞進來的參數之間是共用的。

例如:

function foo(x, y, z) {

// 聲明的函數參數數量arguments (x, y, z)
alert(foo.length); // 3

// 真正傳進來的參數個數(only x, y)
alert(arguments.length); // 2

// 參數的callee是函數自身
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,和參數的第3個索引值是不共用的

z = 40;
alert(arguments[2]); // undefined

arguments[2] = 50;
alert(z); // 40

}

foo(10, 20);

這個例子的代碼,在目前的版本的Google Chrome瀏覽器裡有一個bug  — 即使沒有傳遞參數z,z和arguments[2]仍然是共用的。

處理上下文代碼的2個階段

現在我們終於到了本文的核心點了。執行內容的代碼被分成兩個基本的階段來處理:

  1.     進入執行內容
  2.     執行代碼

變數對象的修改變化與這兩個階段緊密相關。

註:這2個階段的處理是一般行為,和內容相關的類型無關(也就是說,在全域上下文和函數上下文中的表現是一樣的)。

進入執行內容

當進入執行內容(代碼執行之前)時,VO裡已經包含了下列屬性(前面已經說了):

    函數的所有形參(如果我們是在函數執行內容中)

    — 由名稱和對應值組成的一個變數對象的屬性被建立;沒有傳遞對應參數的話,那麼由名稱和undefined值組成的一種變數對象的屬性也將被建立。

    所有函式宣告(FunctionDeclaration, FD)

    —由名稱和對應值(函數對象(function-object))組成一個變數對象的屬性被建立;如果變數對象已經存在相同名稱的屬性,則完全替換這個屬性。

    所有變數聲明(var, VariableDeclaration)

    — 由名稱和對應值(undefined)組成一個變數對象的屬性被建立;如果變數名稱跟已經聲明的形式參數或函數相同,則變數聲明不會干擾已經存在的這類屬性。

讓我們看一個例子:

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: <reference to FunctionDeclaration "d">
e: undefined
};

注意,AO裡並不包含函數“x”。這是因為“x” 是一個函數運算式(FunctionExpression, 縮寫為 FE) 而不是函式宣告,函數運算式不會影響VO。 不管怎樣,函數“_e” 同樣也是函數運算式,但是就像我們下面將看到的那樣,因為它分配給了變數 “e”,所以它可以通過名稱“e”來訪問。 函式宣告FunctionDeclaration與函數運算式FunctionExpression 的不同,將在第15章Functions進行詳細的探討,也可以參考本系列第2章揭秘命名函數運算式來瞭解。

這之後,將進入處理上下文代碼的第二個階段 — 執行代碼。

代碼執行

這個周期內,AO/VO已經擁有了屬性(不過,並不是所有的屬性都有值,大部分屬性的值還是系統預設的初始值undefined )。

還是前面那個例子, AO/VO在代碼解釋期間被修改如下:

AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;

再次注意,因為FunctionExpression“_e”儲存到了已聲明的變數“e”上,所以它仍然存在於記憶體中。而 FunctionExpression “x”卻不存在於AO/VO中,也就是說如果我們想嘗試調用“x”函數,不管在函數定義之前還是之後,都會出現一個錯誤“x is not defined”,未儲存的函數運算式只有在它自己的定義或遞迴中才能被調用。

另一個經典例子:

alert(x); // function

var x = 10;
alert(x); // 10

x = 20;

function x() {};

alert(x); // 20

為什麼第一個alert “x” 的傳回值是function,而且它還是在“x” 聲明之前訪問的“x” 的?為什麼不是10或20呢?因為,根據規範函式宣告是在當進入上下文時填入的; 同意周期,在進入內容相關的時候還有一個變數聲明“x”,那麼正如我們在上一個階段所說,變數聲明在順序上跟在函式宣告和形式參數聲明之後,而且在這個進入上下文階段,變數聲明不會干擾VO中已經存在的同名函式宣告或形式參數聲明,因此,在進入上下文時,VO的結構如下:

VO = {};

VO['x'] = <reference to FunctionDeclaration "x">

// 找到var x = 10;
// 如果function "x"沒有已經聲明的話
// 這時候"x"的值應該是undefined
// 但是這個case裡變數聲明沒有影響同名的function的值

VO['x'] = <the value is not disturbed, still function>

緊接著,在執行代碼階段,VO做如下修改:

VO['x'] = 10;
VO['x'] = 20;

我們可以在第二、三個alert看到這個效果。

在下面的例子裡我們可以再次看到,變數是在進入上下文階段放入VO中的。(因為,雖然else部分代碼永遠不會執行,但是不管怎樣,變數“b”仍然存在於VO中。)

if (true) {
var a = 1;
} else {
var b = 2;
}

alert(a); // 1
alert(b); // undefined,不是b沒有聲明,而是b的值是undefined

關於變數

通常,各類文章和JavaScript相關的書籍都聲稱:“不管是使用var關鍵字(在全域上下文)還是不使用var關鍵字(在任何地方),都可以聲明一個變數”。請記住,這是錯誤的概念:

任何時候,變數只能通過使用var關鍵字才能聲明。

上面的指派陳述式:

a = 10;

這僅僅是給全域對象建立了一個新屬性(但它不是變數)。“不是變數”並不是說它不能被改變,而是指它不符合ECMAScript規範中的變數概念, 所以它“不是變數”(它之所以能成為全域對象的屬性,完全是因為VO(globalContext) === global,大家還記得這個吧?)。

讓我們通過下面的執行個體看看具體的區別吧:

alert(a); // undefined
alert(b); // "b" 沒有聲明

b = 10;
var a = 20;

所有根源仍然是VO和進入上下文階段和代碼執行階段:

進入上下文階段:

VO = {
a: undefined
};

我們可以看到,因為“b”不是一個變數,所以在這個階段根本就沒有“b”,“b”將只在代碼執行階段才會出現(但是在我們這個例子裡,還沒有到那就已經出錯了)。

讓我們改變一下例子代碼:

alert(a); // undefined, 這個大家都知道,

b = 10;
alert(b); // 10, 代碼執行階段建立

var a = 20;
alert(a); // 20, 代碼執行階段修改

關於變數,還有一個重要的知識點。變數相對於簡單屬性來說,變數有一個特性(attribute):{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上下文,變數沒有{DontDelete}特性。

eval('var a = 10;');
alert(window.a); // 10

alert(delete a); // true

alert(window.a); // undefined

使用一些調試工具(例如:Firebug)的控制台測試該執行個體時,請注意,Firebug同樣是使用eval來執行控制台裡你的代碼。因此,變數屬性同樣沒有{DontDelete}特性,可以被刪除。

特殊實現: __parent__ 屬性

前面已經提到過,按標準規範,使用中的物件是不可能被直接存取到的。但是,一些具體實現並沒有完全遵守這個規定,例如SpiderMonkey和 Rhino;的實現中,函數有一個特殊的屬性 __parent__,通過這個屬性可以直接引用到使用中的物件(或全域變數對象),在此對象裡建立了函數。

例如 (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中用同樣的方式訪問使用中的物件是不可能的:在不同版本的SpiderMonkey中,內建函式的__parent__ 有時指向null ,有時指向全域對象。

在Rhino中,用同樣的方式訪問使用中的物件是完全可以的。

例如 (Rhino):

var global = this;
var x = 10;

(function foo() {

var y = 20;

// "foo"上下文裡的使用中的物件
var AO = (function () {}).__parent__;

print(AO.y); // 20

// 當前使用中的物件的__parent__ 是已經存在的全域對象
// 變數對象的特殊鏈形成了
// 所以我們叫做範圍鏈
print(AO.__parent__ === global); // true

print(AO.__parent__.x); // 10

})();

總結

在這篇文章裡,我們深入學習了跟執行內容相關的對象。我希望這些知識對您來說能有所協助,能解決一些您曾經遇到的問題或困惑。按照計劃,在後續的章節中,我們將探討範圍鏈,標識符解析,閉包。

有任何問題,我很高興在下面評論中能幫你解答。

其它參考
  • 10.1.3 – Variable Instantiation;
  • 10.1.5 – Global Object;
  • 10.1.6 – Activation Object;
  • 10.1.8 – Arguments Object.

轉自:湯姆大叔

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.