摘要:所有變量聲明由名稱和對(duì)應(yīng)值組成一個(gè)變量對(duì)象的屬性被創(chuàng)建如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會(huì)干擾已經(jīng)存在的這類屬性。
介紹
JavaScript編程的時(shí)候總避免不了聲明函數(shù)和變量,以成功構(gòu)建我們的系統(tǒng),但是解釋器是如何并且在什么地方去查找這些函數(shù)和變量呢?我們引用這些對(duì)象的時(shí)候究竟發(fā)生了什么?
原始發(fā)布:Dmitry A. Soshnikov 發(fā)布時(shí)間:2009-06-27 俄文地址:http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/
英文翻譯:Dmitry A. Soshnikov 發(fā)布時(shí)間:2010-03-15 英文地址:http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/ 部分難以翻譯的句子參考了justinw的中文翻譯
大多數(shù)ECMAScript程序員應(yīng)該都知道變量與執(zhí)行上下文有密切關(guān)系:
var a = 10; // 全局上下文中的變量 (function () { var b = 20; // function上下文中的局部變量 })(); console.log(a); // 10 console.log(b); // 全局變量 "b" 沒有聲明
并且,很多程序員也都知道,當(dāng)前 ECMAScript 規(guī)范指出獨(dú)立作用域只能通過"函數(shù)(function)"代碼類型的執(zhí)行上下文創(chuàng)建。也就是說,相對(duì)于C/C++來說,ECMAScript 里的 for 循環(huán)并不能創(chuàng)建一個(gè)局部的上下文。
for (var k in {a: 1, b: 2}) { console.log(k); } console.log(k); // 盡管循環(huán)已經(jīng)結(jié)束但變量k依然在當(dāng)前作用域
我們來看看一下,我們聲明數(shù)據(jù)的時(shí)候到底都發(fā)現(xiàn)了什么細(xì)節(jié)。
數(shù)據(jù)聲明如果變量與執(zhí)行上下文相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存儲(chǔ)在哪里,并且知道如何訪問。這種機(jī)制稱為變量對(duì)象(variable object)。
變量對(duì)象(縮寫為VO)是一個(gè)與執(zhí)行上下文相關(guān)的特殊對(duì)象,它存儲(chǔ)著在上下文中聲明的以下內(nèi)容: 變量 (var, 變量聲明); 函數(shù)聲明 (FunctionDeclaration, 縮寫為FD); 函數(shù)的形參
舉例來說,我們可以用普通的 ECMAScript 對(duì)象來表示一個(gè)變量對(duì)象:
VO = {};
就像我們所說的, VO 就是執(zhí)行上下文的屬性( property ):
activeExecutionContext = { VO: { // 上下文數(shù)據(jù)(var, FD, function arguments) } };
只有全局上下文的變量對(duì)象允許通過 VO 的屬性名稱來間接訪問(因?yàn)樵谌稚舷挛睦铮謱?duì)象自身就是變量對(duì)象,稍后會(huì)詳細(xì)介紹),在其它上下文中是不能直接訪問 VO 對(duì)象的,因?yàn)樗皇莾?nèi)部機(jī)制的一個(gè)實(shí)現(xiàn)。
當(dāng)我們聲明一個(gè)變量或一個(gè)函數(shù)的時(shí)候,和我們創(chuàng)建 VO 新屬性的時(shí)候一樣沒有別的區(qū)別(即:有名稱以及對(duì)應(yīng)的值)。
例如:
var a = 10; function test(x) { var b = 20; }; test(30);
對(duì)應(yīng)的變量對(duì)象是:
// 全局上下文的變量對(duì)象 VO(globalContext) = { a: 10, test:}; // test函數(shù)上下文的變量對(duì)象 VO(test functionContext) = { x: 30, b: 20 };
在具體實(shí)現(xiàn)層面(以及規(guī)范中)變量對(duì)象只是一個(gè)抽象概念。(從本質(zhì)上說,在具體執(zhí)行上下文中,VO 名稱是不一樣的,并且初始結(jié)構(gòu)也不一樣。
不同執(zhí)行上下文中的變量對(duì)象對(duì)于所有類型的執(zhí)行上下文來說,變量對(duì)象的一些操作(如變量初始化)和行為都是共通的。從這個(gè)角度來看,把變量對(duì)象作為抽象的基本事物來理解更為容易。同樣在函數(shù)上下文中也定義和變量對(duì)象相關(guān)的額外內(nèi)容。
抽象變量對(duì)象VO (變量初始化過程的一般行為) ║ ╠══> 全局上下文變量對(duì)象GlobalContextVO ║ (VO === this === global) ║ ╚══> 函數(shù)上下文變量對(duì)象FunctionContextVO (VO === AO, 并且添加了
和 )
我們來詳細(xì)看一下:
全局上下文中的變量對(duì)象首先,我們要給全局對(duì)象一個(gè)明確的定義:
全局對(duì)象( Global object ) 是在進(jìn)入任何執(zhí)行上下文之前就已經(jīng)創(chuàng)建了的對(duì)象; 這個(gè)對(duì)象只存在一份,它的屬性在程序中任何地方都可以訪問,全局對(duì)象的生命周期終止于程序退出那一刻。
全局對(duì)象初始創(chuàng)建階段將Math、String、Date、parseInt作為自身屬性,等屬性初始化,同樣也可以有額外創(chuàng)建的其它對(duì)象作為屬性(其可以指向到全局對(duì)象自身)。例如,在DOM中,全局對(duì)象的window屬性就可以引用全局對(duì)象自身(當(dāng)然,并不是所有的具體實(shí)現(xiàn)都是這樣):
global = { Math: <...>, String: <...> ... ... window: global //引用自身 };
當(dāng)訪問全局對(duì)象的屬性時(shí)通常會(huì)忽略掉前綴,這是因?yàn)槿謱?duì)象是不能通過名稱直接訪問的。不過我們依然可以通過全局上下文的this來訪問全局對(duì)象,同樣也可以遞歸引用自身。例如,DOM中的window。綜上所述,代碼可以簡(jiǎn)寫為:
String(10); // 就是global.String(10); // 帶有前綴 window.a = 10; // === global.window.a = 10 === global.a = 10; this.b = 20; // global.b = 20;
因此,回到全局上下文中的變量對(duì)象----在這里,變量對(duì)象就是全局對(duì)象自己:
VO(globalContext) === global;
非常有必要要理解上述結(jié)論,基于這個(gè)原理,在全局上下文中聲明的對(duì)應(yīng),我們才可以間接通過全局對(duì)象的屬性來訪問它(例如,事先不知道變量名稱)。
var a = new String("test"); console.log(a); // 直接訪問,在VO(globalContext)里找到:"test" console.log(window["a"]); // 間接通過global訪問:global === VO(globalContext): "test" console.log(a === this.a); // true var aKey = "a"; console.log(window[aKey]); // 間接通過動(dòng)態(tài)屬性名稱訪問:"test"函數(shù)上下文中的變量對(duì)象
在函數(shù)執(zhí)行上下文中,VO是不能直接訪問的,此時(shí)由活動(dòng)對(duì)象(activation object,縮寫為AO)扮演VO的角色。
VO(functionContext) === AO;
活動(dòng)對(duì)象是在進(jìn)入函數(shù)上下文時(shí)刻被創(chuàng)建的,它通過函數(shù)的arguments屬性初始化。arguments屬性的值是Arguments對(duì)象:
AO = { arguments:};
Arguments對(duì)象是活動(dòng)對(duì)象的一個(gè)屬性,它包括如下屬性:
callee -- 指向當(dāng)前函數(shù)的引用
length -- 真正傳遞的參數(shù)個(gè)數(shù)
properties-indexes (字符串類型的整數(shù)) 屬性的值就是函數(shù)的參數(shù)值(按參數(shù)列表從左到右排列)。 properties-indexes內(nèi)部元素的個(gè)數(shù)等于arguments.length. properties-indexes 的值和實(shí)際傳遞進(jìn)來的參數(shù)之間是共享的。
例如:
function foo(x, y, z) { // 聲明的函數(shù)參數(shù)數(shù)量arguments (x, y, z) console.log(foo.length); // 3 // 真正傳進(jìn)來的參數(shù)個(gè)數(shù)(only x, y) console.log(arguments.length); // 2 // 參數(shù)的callee是函數(shù)自身 console.log(arguments.callee === foo); // true // 參數(shù)共享 console.log(x === arguments[0]); // true console.log(x); // 10 arguments[0] = 20; console.log(x); // 20 x = 30; console.log(arguments[0]); // 30 // 不過,沒有傳進(jìn)來的參數(shù)z,和參數(shù)的第3個(gè)索引值是不共享的 z = 40; console.log(arguments[2]); // undefined arguments[2] = 50; console.log(z); // 40 } foo(10, 20);
這個(gè)例子的代碼,在當(dāng)前版本的Google Chrome瀏覽器里有一個(gè)bug -- 即使沒有傳遞參數(shù)z,z和arguments[2]仍然是共享的。
處理上下文代碼的2個(gè)階段現(xiàn)在我們終于到了本文的核心點(diǎn)了。執(zhí)行上下文的代碼被分成兩個(gè)基本的階段來處理:
進(jìn)入執(zhí)行上下文
執(zhí)行代碼
變量對(duì)象的修改變化與這兩個(gè)階段緊密相關(guān)。
注:這2個(gè)階段的處理是一般行為,和上下文的類型無關(guān)(也就是說,在全局上下文和函數(shù)上下文中的表現(xiàn)是一樣的)。
進(jìn)入執(zhí)行上下文當(dāng)進(jìn)入執(zhí)行上下文(代碼執(zhí)行之前)時(shí),VO里已經(jīng)包含了下列屬性(前面已經(jīng)說了): 函數(shù)的所有形參(如果我們是在函數(shù)執(zhí)行上下文中) -- 由名稱和對(duì)應(yīng)值組成的一個(gè)變量對(duì)象的屬性被創(chuàng)建;沒有傳遞對(duì)應(yīng)參數(shù)的話,那么由名稱和undefined值組成的一種變量對(duì)象的屬性也將被創(chuàng)建。 所有函數(shù)聲明(FunctionDeclaration, FD) --由名稱和對(duì)應(yīng)值(函數(shù)對(duì)象(function-object))組成一個(gè)變量對(duì)象的屬性被創(chuàng)建;如果變量對(duì)象已經(jīng)存在相同名稱的屬性,則完全替換這個(gè)屬性。 所有變量聲明(var, VariableDeclaration) -- 由名稱和對(duì)應(yīng)值(undefined)組成一個(gè)變量對(duì)象的屬性被創(chuàng)建;如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會(huì)干擾已經(jīng)存在的這類屬性。
讓我們看一個(gè)例子:
function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {}); } test(10); // call
當(dāng)進(jìn)入帶有參數(shù)10的test函數(shù)上下文時(shí),AO表現(xiàn)為如下:
AO(test) = { a: 10, b: undefined, c: undefined, d:e: undefined };
注意,AO里并不包含函數(shù)"x"。這是因?yàn)?x" 是一個(gè)函數(shù)表達(dá)式(FunctionExpression, 縮寫為 FE) 而不是函數(shù)聲明,函數(shù)表達(dá)式不會(huì)影響VO。 不管怎樣,函數(shù)"_e" 同樣也是函數(shù)表達(dá)式,但是就像我們下面將看到的那樣,因?yàn)樗峙浣o了變量 "e",所以它可以通過名稱"e"來訪問。 函數(shù)聲明FunctionDeclaration與函數(shù)表達(dá)式FunctionExpression 的不同,將在第15章Functions進(jìn)行詳細(xì)的探討,也可以參考本系列第2章揭秘命名函數(shù)表達(dá)式來了解。
這之后,將進(jìn)入處理上下文代碼的第二個(gè)階段 -- 執(zhí)行代碼。
代碼執(zhí)行這個(gè)周期內(nèi),AO/VO已經(jīng)擁有了屬性(不過,并不是所有的屬性都有值,大部分屬性的值還是系統(tǒng)默認(rèn)的初始值undefined )。
還是前面那個(gè)例子, AO/VO在代碼解釋期間被修改如下:
AO["c"] = 10; AO["e"] =;
再次注意,因?yàn)镕unctionExpression"_e"保存到了已聲明的變量"e"上,所以它仍然存在于內(nèi)存中。而FunctionExpression "x"卻不存在于AO/VO中,也就是說如果我們想嘗試調(diào)用"x"函數(shù),不管在函數(shù)定義之前還是之后,都會(huì)出現(xiàn)一個(gè)錯(cuò)誤"x is not defined",未保存的函數(shù)表達(dá)式只有在它自己的定義或遞歸中才能被調(diào)用。
另一個(gè)經(jīng)典例子:
console.log(x); // function var x = 10; console.log(x); // 10 x = 20; function x() {}; console.log(x); // 20
為什么第一個(gè)console.log "x" 的返回值是function,而且它還是在"x" 聲明之前訪問的"x" 的?為什么不是10或20呢?因?yàn)椋鶕?jù)規(guī)范函數(shù)聲明是在當(dāng)進(jìn)入上下文時(shí)填入的; 同意周期,在進(jìn)入上下文的時(shí)候還有一個(gè)變量聲明"x",那么正如我們?cè)谏弦粋€(gè)階段所說,變量聲明在順序上跟在函數(shù)聲明和形式參數(shù)聲明之后,而且在這個(gè)進(jìn)入上下文階段,變量聲明不會(huì)干擾VO中已經(jīng)存在的同名函數(shù)聲明或形式參數(shù)聲明,因此,在進(jìn)入上下文時(shí),VO的結(jié)構(gòu)如下:
VO = {}; VO["x"] =// 找到var x = 10; // 如果function "x"沒有已經(jīng)聲明的話 // 這時(shí)候"x"的值應(yīng)該是undefined // 但是這個(gè)case里變量聲明沒有影響同名的function的值 VO["x"] =
緊接著,在執(zhí)行代碼階段,VO做如下修改:
VO["x"] = 10; VO["x"] = 20;
我們可以在第二、三個(gè)console.log看到這個(gè)效果。
在下面的例子里我們可以再次看到,變量是在進(jìn)入上下文階段放入VO中的。(因?yàn)椋m然else部分代碼永遠(yuǎn)不會(huì)執(zhí)行,但是不管怎樣,變量"b"仍然存在于VO中。)
if (true) { var a = 1; } else { var b = 2; } console.log(a); // 1 console.log(b); // undefined,不是b沒有聲明,而是b的值是undefined關(guān)于變量
通常,各類文章和JavaScript相關(guān)的書籍都聲稱:"不管是使用var關(guān)鍵字(在全局上下文)還是不使用var關(guān)鍵字(在任何地方),都可以聲明一個(gè)變量"。請(qǐng)記住,這是 錯(cuò)誤 的概念: 任何時(shí)候,變量只能通過使用var關(guān)鍵字才能聲明。
上面的賦值語句:
a = 10;
這僅僅是給全局對(duì)象創(chuàng)建了一個(gè)新屬性(但它不是變量)。"不是變量"并不是說它不能被改變,而是指它不符合ECMAScript規(guī)范中的變量概念,所以它"不是變量"(它之所以能成為全局對(duì)象的屬性,完全是因?yàn)閂O(globalContext) === global,大家還記得這個(gè)吧?)。
讓我們通過下面的實(shí)例看看具體的區(qū)別吧:
console.log(a); // undefined console.log(b); // "b" 沒有聲明 b = 10; var a = 20;
所有根源仍然是VO和進(jìn)入上下文階段和代碼執(zhí)行階段:
進(jìn)入上下文階段:
VO = { a: undefined };
我們可以看到,因?yàn)?b"不是一個(gè)變量,所以在這個(gè)階段根本就沒有"b","b"將只在代碼執(zhí)行階段才會(huì)出現(xiàn)(但是在我們這個(gè)例子里,還沒有到那就已經(jīng)出錯(cuò)了)。
讓我們改變一下例子代碼:
console.log(a); // undefined, 這個(gè)大家都知道, b = 10; console.log(b); // 10, 代碼執(zhí)行階段創(chuàng)建 var a = 20; console.log(a); // 20, 代碼執(zhí)行階段修改
關(guān)于變量,還有一個(gè)重要的知識(shí)點(diǎn)。變量相對(duì)于簡(jiǎn)單屬性來說,變量有一個(gè)特性(attribute):{DontDelete},這個(gè)特性的含義就是不能用delete操作符直接刪除變量屬性。
a = 10; console.log(window.a); // 10 console.log(delete a); // true console.log(window.a); // undefined var b = 20; console.log(window.b); // 20 console.log(delete b); // false console.log(window.b); // still 20
但是這個(gè)規(guī)則在有個(gè)上下文里不起走樣,那就是eval上下文,變量沒有{DontDelete}特性。
eval("var a = 10;"); console.log(window.a); // 10 console.log(delete a); // true console.log(window.a); // undefined
使用一些調(diào)試工具(例如:Firebug)的控制臺(tái)測(cè)試該實(shí)例時(shí),請(qǐng)注意,F(xiàn)irebug同樣是使用eval來執(zhí)行控制臺(tái)里你的代碼。因此,變量屬性同樣沒有{DontDelete}特性,可以被刪除。
特殊實(shí)現(xiàn): parent 屬性前面已經(jīng)提到過,按標(biāo)準(zhǔn)規(guī)范,活動(dòng)對(duì)象是不可能被直接訪問到的。但是,一些具體實(shí)現(xiàn)并沒有完全遵守這個(gè)規(guī)定,例如SpiderMonkey和Rhino;的實(shí)現(xiàn)中,函數(shù)有一個(gè)特殊的屬性 parent,通過這個(gè)屬性可以直接引用到活動(dòng)對(duì)象(或全局變量對(duì)象),在此對(duì)象里創(chuàng)建了函數(shù)。
例如 (SpiderMonkey, Rhino):
var global = this; var a = 10; function foo() {} console.log(foo.__parent__); // global var VO = foo.__parent__; console.log(VO.a); // 10 console.log(VO === global); // true
在上面的例子中我們可以看到,函數(shù)foo是在全局上下文中創(chuàng)建的,所以屬性parent 指向全局上下文的變量對(duì)象,即全局對(duì)象。
然而,在SpiderMonkey中用同樣的方式訪問活動(dòng)對(duì)象是不可能的:在不同版本的SpiderMonkey中,內(nèi)部函數(shù)的parent 有時(shí)指向null ,有時(shí)指向全局對(duì)象。
在Rhino中,用同樣的方式訪問活動(dòng)對(duì)象是完全可以的。
例如 (Rhino):
var global = this; var x = 10; (function foo() { var y = 20; // "foo"上下文里的活動(dòng)對(duì)象 var AO = (function () {}).__parent__; print(AO.y); // 20 // 當(dāng)前活動(dòng)對(duì)象的__parent__ 是已經(jīng)存在的全局對(duì)象 // 變量對(duì)象的特殊鏈形成了 // 所以我們叫做作用域鏈 print(AO.__parent__ === global); // true print(AO.__parent__.x); // 10 })();總結(jié)
在這篇文章里,我們深入學(xué)習(xí)了跟執(zhí)行上下文相關(guān)的對(duì)象。我希望這些知識(shí)對(duì)您來說能有所幫助,能解決一些您曾經(jīng)遇到的問題或困惑。按照計(jì)劃,在后續(xù)的章節(jié)中,我們將探討作用域鏈,標(biāo)識(shí)符解析,閉包。
有任何問題,我很高興在下面評(píng)論中能幫你解答。
其它參考10.1.3 – Variable Instantiation;
10.1.5 – Global Object;
10.1.6 – Activation Object;
10.1.8 – Arguments Object.
關(guān)于本文本文轉(zhuǎn)自TOM大叔的深入理解JavaScript系列。聲明一下,本人所有整理的文章均不是照搬全抄,加入自己的理解和詳細(xì)的注解,以及修改了一些語病錯(cuò)字等。
【深入理解JavaScript系列】文章,包括了原創(chuàng),翻譯,轉(zhuǎn)載,整理等各類型文章,原文是TOM大叔的一個(gè)非常不錯(cuò)的專題,現(xiàn)將其重新整理發(fā)布。謝謝大叔。如果你覺得本文不錯(cuò),請(qǐng)幫忙點(diǎn)個(gè)推薦,支持一把,感激不盡。
更多優(yōu)秀文章歡迎關(guān)注我的專欄
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/79216.html
摘要:總結(jié)上面的大部分方式都可以互相組合使用的,一般來說如果要設(shè)計(jì)系統(tǒng),可能會(huì)用到松耦合擴(kuò)展,私有狀態(tài)和子模塊這樣的方式。 簡(jiǎn)介 Module模式是JavaScript編程中一個(gè)非常通用的模式,一般情況下,大家都知道基本用法,本文嘗試著給大家更多該模式的高級(jí)使用方式。 首先我們來看看Module模式的基本特征: 模塊化,可重用 封裝了變量和function,和全局的namaspace不接觸...
摘要:前言本章我們要講解的是五大原則語言實(shí)現(xiàn)的第篇,依賴倒置原則。當(dāng)應(yīng)用依賴倒置原則的時(shí)候,關(guān)系就反過來了。在當(dāng)靜態(tài)類型語言的上下文里討論依賴倒置原則的時(shí)候,耦合的概念包括語義和物理兩種。依賴倒置原則和依賴注入都是關(guān)注依賴,并且都是用于反轉(zhuǎn)。 前言 本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實(shí)現(xiàn)的第5篇,依賴倒置原則LSP(The Dependency Invers...
摘要:編寫高質(zhì)量的要點(diǎn)深入理解系列一知識(shí)點(diǎn)最小全局變量全局變量命名易與第三方的腳本引起沖突所以盡可能少的使用全局變量是很重要的相關(guān)策略有命名空間模式或是函數(shù)立即自動(dòng)執(zhí)行,但是要想讓全局變量少最重要的還是始終使用來聲明變量。 Title: 編寫高質(zhì)量Javascript的要點(diǎn)-Review深入理解Javascript系列(一)date: 2017-6-9 14:14:20 status: p...
摘要:使用上一篇文章的例子來說明下自由變量進(jìn)階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數(shù),也不是局部變量,所以是自由變量。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第7天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)...
摘要:函數(shù)式編程前端掘金引言面向?qū)ο缶幊桃恢币詠矶际侵械闹鲗?dǎo)范式。函數(shù)式編程是一種強(qiáng)調(diào)減少對(duì)程序外部狀態(tài)產(chǎn)生改變的方式。 JavaScript 函數(shù)式編程 - 前端 - 掘金引言 面向?qū)ο缶幊桃恢币詠矶际荍avaScript中的主導(dǎo)范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數(shù)式編程越來越多得受到開發(fā)者的青睞。函數(shù)式編程是一種強(qiáng)調(diào)減少對(duì)程序外部狀態(tài)產(chǎn)生改變的方式。因此,...
閱讀 1529·2021-11-22 09:34
閱讀 3329·2021-09-29 09:35
閱讀 576·2021-09-04 16:40
閱讀 2919·2019-08-30 15:53
閱讀 2594·2019-08-30 15:44
閱讀 2591·2019-08-30 14:10
閱讀 1335·2019-08-29 18:43
閱讀 2215·2019-08-29 13:26