DS.Lab筆記 - ECMA-262-3: 變量體(variable object)
2017-03-14 17:57
411 查看
原文鏈接:ECMA-262-3 in detail. Chapter 2. Variable object.
首先第一點,變量體(variable object)一定是相對於執行上下文(execution context)而存在的,它自己不會獨立存在,一個變量體一定是從屬於某一個執行上下文的,它的目的就是幫助這個執行上下文存儲下列內容:
變量(VariableDeclaration)
函數聲明(FunctionDeclaration)
函數的形式參數(function formal parameters)
最後一項只適用於函數的執行上下文上面的變量體(前面的第一章講過了,執行上下文有兩種,全局的和函數的,全局的當然不可能有參數傳進來)。
咱們用JavaScript自己的語法標記來統一點地表達下就是:
=============================================================
那現在咱們假設一下如下代碼:
這段代碼的執行會導致兩個執行上下文的生成,分別是全局執行上下文和test函數的執行上下文,表示的話,它們將會是這樣的:
目前,除了<reference to function>這個東西的深層表達和具體實現不太懂之外,so far so good。
=============================================================
下面的話題就是變量體在不同類型的執行上下文里究竟什麼樣了。首先是全局執行上下文,這個情況比較簡單,全局對象自身就是全局執行上下文里的變量體。那麼,全局對象是什麼?其實就是在全局作用域里使用this的時候它指向的那個東西,用window也是指向它。這個對象有幾個特征:
在JavaScript被執行的時候首先被創建,程序被關閉后才會被銷毀,所以它的生命週期是整個JS程序,在任何地方都能被訪問到;
它是單例模式,整個週期里只此一個。
它的特殊的實現機制令它還附帶一個福利,就是可以用window['a']這樣的代碼訪問變量,所以你就能動態地訪問編譯時還未知命名的變量,像下面這樣:
至於為什麼全局對象就可以這樣操作,作者沒有給出深入的解釋。
另外它初始化的時候,也就是Math,String這樣的內建類對象被創建的時候:
現在說說函數上下文里的變量體,作者首先說在這種情況下,“變量體扮演了觸發體(activation object)的角色。”之後,在這一節作者就基本上用觸發體這個概念取代變量體了,既然更換了名詞,表示在某些方面兩者有本質上的差異。另外作者還用了一個公式來解釋了下兩者的關係:
VO(functionContext) === AO;
還有:
AbstractVO (generic behavior of the variable instantiation process)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)
其實這給我感覺語焉不詳,從這個拓撲圖來看,我覺得是在函數上下文的變量體包括了觸髮體再加上後面兩個東西。後來也有人在留言里問,提出一種解讀方式是:變量體和觸發體是父類和子類的關係,後者有前者所有的行為,并加上一些自己的獨特行為,作者也認可這種解讀,暫且只知道這麼多了。可是從後來的描述看,參數相關的信息是被保存在觸髮體下面的。
=============================================================
這段代碼首先展示了arguments上面的屬性的用法。另外作者想指出的一個問題是,對於沒有傳進來的參數,比如z,它與arguments[2]並不相互關聯,它們應該分別指向不同的變量,所以修改一個不會影響另一個。注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
=============================================================
運行代碼。
這個過程對於全局上下文或者函數上下文是相同的。
在變量體上給每個函數聲明創建一個對應的屬性,屬性名相同于函數名,如果該名字已經有了,就重寫它原來的值,注意,這個是僅僅相對於普通變量而言,它不會重寫形式參數,參加後面的實驗;
在變量體上給每個變量聲明創建一個對應的屬性,屬性名相同于變量名,重點是如果該名字已經有了,就什麼都不發生,所以說變量聲明的優先級最低。
作者給了個例子:
有一些需要注意的是,函數表達式和函數聲明並不同(這個後面第五章會談到),這裡面的_e和x都是函數表達式,不是函數聲明,所以它們並不影響變量體。因此,test()的上下文的變量體應該是:
=============================================================
一個經典面試問題的解讀
首先,這是個全局上下文,沒有形式參數,那就看函數聲明,這裡只有一個函數聲明就是x函數,所以在進入上下文階段里,在變量體上有一個叫x的屬性,然後是有個同名的變量聲明,因為優先級低,所以被忽略。然後進入執行的階段,alert(x)會輸出的是“function”。然後x被賦值10,於是第二個alert(x)輸出的是10,然後又被賦值20,所以最後的alert(x)輸出的是20。注:這段代碼在Chrome
(Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
由於前面提到的處理執行上下文的步驟里會對變量體的操作,雖然else代碼塊不會被執行,但是在進入上下文階段,它裡面的變量b卻是會被放到變量體上的,而且它的賦值是undefined,所以alert(b)輸出的是undefined。注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
對執行上下文的這個處理過程所導致的現象,比如變量會被在執行前先被聲明,好像變量聲明被預先處理過了,還有形式參數,函數聲明和變量聲明之間的優先級的差異性,這些東西往往在一些教材里被稱作變量提升(variable hoisting),而且他們解釋這個東西的方式會讓人覺得它是這些表象背後的運行機制,比如【You
Don't Know JS - Scope and Closures】的第四章。然而其實它們是真正的背後機制導致的表象而已。這裡解釋的才是真正的“背後的原理”。
但是在【Effective JavaScript】里,作者對這個概念的界定似乎合理一些:由於不支持代碼塊作用域,在一個像if和for這樣的代碼塊里聲明的變量的作用域會被提升到包含它的函數的作用域範圍。這個解釋聽起來才像是對一個現象的描述。
=============================================================
先從概念上說,忽略var關鍵字的結果是在全局對象上創建了一個屬性,比如說上面的例子會創建window.a。而用var聲明的變量則是被存放在變量體上的,而兩者的區別不僅表現在這種概念原理上,也有一些外在的表象上的不同,比如:
alert(b)導致一個引用異常被拋出,因為它是一個全局對象的屬性,在處理執行上下文的過程里它沒有影響到變量體,變量體上面找不到名字為b的屬性。
另外有一個會對寫程序造成實質影響的區別是可刪除性,對象上的屬性可以通過delete移除,而變量卻不可以。所以通過var關鍵字聲明的變量是不能用delete操作符移除的。下面的代碼演示了這個問題:
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。另外有一個特例,eval上下文里不會給變量添加{DontDelete}屬性,所以eval上下文里的變量可以被刪除。
__parent__ 屬性(略,因為現在在Chrome和Firefox里都不支持,看不見的東西,討論也沒有意義)
=============================================================
有個人在提到一個關於var關鍵字的問題時,引起了我的一個想法,當我們在函數里使用a=10時,其實是效果是等同於window.a=10,前面已經解釋過了,我在想的是,這會不會是因為函數里的this指向的是window,也就是全局對象,因為所有演示這個問題的代碼使用的都是全局作用域里聲明的一個函數並且在隨後即調用。所以我在揣測,會不會其實原則是:在this指向的對象上創建沒有用var關鍵字的賦值操作,所以,因為在這些例子中this都是指向全局對象的,所以a才在window上被創建。
先通過下面代碼再次演示這個問題,並且把它作為參照組:注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
我設計了下面的代碼來更改一個函數執行時this指向的對象:
這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同。從結果來看,我的設想是錯誤的,不論this指向什麼,a=10這樣的代碼都會在window上創建a屬性。
問題二.
有人針對各種示例代碼中出現的一個現象問了個問題,如代碼:
明明在全局對象上沒有定義x屬性,可是嘗試訪問它得到的是undefined,可是訪問沒有聲明的變量得到的卻是一個引用異常被拋出。關於這個問題,作者的解釋是,這就是標識符解析(identifier resolution)的規則,沒有太複雜的原因,在一個對象上找不到某個屬性就默認返回undefined,而找不到一個變量就拋出異常。
問題三.
有人發現了一個新的情況:
這種情況下這個y怎麼算?放在myFunction上下文的觸發體上?答案是y既然定義在myFunction上,那麼它就放在myFunction對象上,函數也是對象嘛,它跟觸發體不會發生直接關係。所以觸髮體的狀態是:
問題四.
這個問題應該在後面說會好一些,它屬於作用域的問題。不過既然作者在評論里說了,那就先在這裡講一下。基本上大家都知道JavaScript沒有代碼塊作用域(block scope),很多面試題也是考這個點,比如說if-else代碼塊和for循環代碼塊里創建的變量,其實在這些聲明它們的區塊以外都能訪問到的。作者給了個例子,在Chrome (Version 56.0.2924.87)和Firefox
(51.0.1 (32-bit))中測試的結果都相同:
但是,正如作者指出,有一個特別,就是catch代碼塊,傳給它的參數只會在catch區塊內有效,所以它的作用域是代碼塊級別的(在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同)。
首先第一點,變量體(variable object)一定是相對於執行上下文(execution context)而存在的,它自己不會獨立存在,一個變量體一定是從屬於某一個執行上下文的,它的目的就是幫助這個執行上下文存儲下列內容:
變量(VariableDeclaration)
函數聲明(FunctionDeclaration)
函數的形式參數(function formal parameters)
最後一項只適用於函數的執行上下文上面的變量體(前面的第一章講過了,執行上下文有兩種,全局的和函數的,全局的當然不可能有參數傳進來)。
咱們用JavaScript自己的語法標記來統一點地表達下就是:
activeExecutionContext = { //其他的上下文數據,後面會慢慢講到 VO: { <function argument name>: <function argument real value>, <variable name>: <variable value>, <function name of declaration>: <reference to function> } };
=============================================================
那現在咱們假設一下如下代碼:
var a = 10; function test(x) { var b = 20; }; test(30);
這段代碼的執行會導致兩個執行上下文的生成,分別是全局執行上下文和test函數的執行上下文,表示的話,它們將會是這樣的:
// Variable object of the global context VO(globalContext) = { a: 10, test: <reference to function> }; // Variable object of the "test" function context VO(test functionContext) = { x: 30, b: 20 };
目前,除了<reference to function>這個東西的深層表達和具體實現不太懂之外,so far so good。
=============================================================
下面的話題就是變量體在不同類型的執行上下文里究竟什麼樣了。首先是全局執行上下文,這個情況比較簡單,全局對象自身就是全局執行上下文里的變量體。那麼,全局對象是什麼?其實就是在全局作用域里使用this的時候它指向的那個東西,用window也是指向它。這個對象有幾個特征:
在JavaScript被執行的時候首先被創建,程序被關閉后才會被銷毀,所以它的生命週期是整個JS程序,在任何地方都能被訪問到;
它是單例模式,整個週期里只此一個。
它的特殊的實現機制令它還附帶一個福利,就是可以用window['a']這樣的代碼訪問變量,所以你就能動態地訪問編譯時還未知命名的變量,像下面這樣:
var a = new String('test'); var aKey = 'a'; alert(window[aKey]);
至於為什麼全局對象就可以這樣操作,作者沒有給出深入的解釋。
另外它初始化的時候,也就是Math,String這樣的內建類對象被創建的時候:
global = { Math: <...>, String: <...> ... ... window: global };
現在說說函數上下文里的變量體,作者首先說在這種情況下,“變量體扮演了觸發體(activation object)的角色。”之後,在這一節作者就基本上用觸發體這個概念取代變量體了,既然更換了名詞,表示在某些方面兩者有本質上的差異。另外作者還用了一個公式來解釋了下兩者的關係:
VO(functionContext) === AO;
還有:
AbstractVO (generic behavior of the variable instantiation process)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)
其實這給我感覺語焉不詳,從這個拓撲圖來看,我覺得是在函數上下文的變量體包括了觸髮體再加上後面兩個東西。後來也有人在留言里問,提出一種解讀方式是:變量體和觸發體是父類和子類的關係,後者有前者所有的行為,并加上一些自己的獨特行為,作者也認可這種解讀,暫且只知道這麼多了。可是從後來的描述看,參數相關的信息是被保存在觸髮體下面的。
AO = { arguments: { callee, //就是當前這個執行上下文的函數; length, //就是實際傳進來的參數的個數; properties-indexes // 這個東西就像C#里的索引器,可以像訪問數組一樣訪問arguments,元素就是傳進來的實際參數,細節等下再說; }, <function argument name>: <function argument real value>, <variable name>: <variable value>, <function name of declaration>: <reference to function> };
=============================================================
實際參數與arguments索引器的共享問題
先觀察如下代碼:function foo(x, y, z) { // quantity of defined function arguments (x, y, z) alert(foo.length); // 3 // quantity of really passed arguments (only x, y) alert(arguments.length); // 2 // reference of a function to itself alert(arguments.callee === foo); // true // parameters sharing 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);
這段代碼首先展示了arguments上面的屬性的用法。另外作者想指出的一個問題是,對於沒有傳進來的參數,比如z,它與arguments[2]並不相互關聯,它們應該分別指向不同的變量,所以修改一個不會影響另一個。注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
=============================================================
處理上下文代碼的兩個步驟
進入執行上下文;運行代碼。
這個過程對於全局上下文或者函數上下文是相同的。
進入執行上下文時的步驟是:
(如果是函數上下文)在變量體上給每個形式參數創建一個對應的屬性,屬性名相同于形式參數名,對於還沒有傳進來的,就賦值為undefined;在變量體上給每個函數聲明創建一個對應的屬性,屬性名相同于函數名,如果該名字已經有了,就重寫它原來的值,注意,這個是僅僅相對於普通變量而言,它不會重寫形式參數,參加後面的實驗;
在變量體上給每個變量聲明創建一個對應的屬性,屬性名相同于變量名,重點是如果該名字已經有了,就什麼都不發生,所以說變量聲明的優先級最低。
作者給了個例子:
function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {}); } test(10); // call
有一些需要注意的是,函數表達式和函數聲明並不同(這個後面第五章會談到),這裡面的_e和x都是函數表達式,不是函數聲明,所以它們並不影響變量體。因此,test()的上下文的變量體應該是:
AO(test) = { a: 10, b: undefined, c: undefined, d: <reference to FunctionDeclaration "d"> e: undefined };
運行代碼時的步驟是:
沒啥特別好說的,就一句接著一句線性執行唄。接著上面的代碼例子,到了這個階段執行以後的變量體就變成:AO['c'] = 10; AO['e'] = <reference to FunctionExpression "_e">;
=============================================================
一個經典面試問題的解讀
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {} alert(x); // 20
首先,這是個全局上下文,沒有形式參數,那就看函數聲明,這裡只有一個函數聲明就是x函數,所以在進入上下文階段里,在變量體上有一個叫x的屬性,然後是有個同名的變量聲明,因為優先級低,所以被忽略。然後進入執行的階段,alert(x)會輸出的是“function”。然後x被賦值10,於是第二個alert(x)輸出的是10,然後又被賦值20,所以最後的alert(x)輸出的是20。注:這段代碼在Chrome
(Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
另外一個經典情況
if (true) { var a = 1; } else { var b = 2; } alert(a); // 1 alert(b); // undefined, but not "b is not defined" alert(c); // ReferenceError: c is not defined
由於前面提到的處理執行上下文的步驟里會對變量體的操作,雖然else代碼塊不會被執行,但是在進入上下文階段,它裡面的變量b卻是會被放到變量體上的,而且它的賦值是undefined,所以alert(b)輸出的是undefined。注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
對執行上下文的這個處理過程所導致的現象,比如變量會被在執行前先被聲明,好像變量聲明被預先處理過了,還有形式參數,函數聲明和變量聲明之間的優先級的差異性,這些東西往往在一些教材里被稱作變量提升(variable hoisting),而且他們解釋這個東西的方式會讓人覺得它是這些表象背後的運行機制,比如【You
Don't Know JS - Scope and Closures】的第四章。然而其實它們是真正的背後機制導致的表象而已。這裡解釋的才是真正的“背後的原理”。
但是在【Effective JavaScript】里,作者對這個概念的界定似乎合理一些:由於不支持代碼塊作用域,在一個像if和for這樣的代碼塊里聲明的變量的作用域會被提升到包含它的函數的作用域範圍。這個解釋聽起來才像是對一個現象的描述。
=============================================================
關於變量
網上有很多人說可以在任何地方通過忽略var關鍵字的方式聲明全局變量,比如在一個函數體里,你就用a=2就可以在全局對象上聲明一個名字為a的變量。作者首先評論了這個觀點,這是個誤解!迷思!先從概念上說,忽略var關鍵字的結果是在全局對象上創建了一個屬性,比如說上面的例子會創建window.a。而用var聲明的變量則是被存放在變量體上的,而兩者的區別不僅表現在這種概念原理上,也有一些外在的表象上的不同,比如:
alert(a); // undefined alert(b); // "b" is not defined b = 10; var a = 20;
alert(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
另外有一個會對寫程序造成實質影響的區別是可刪除性,對象上的屬性可以通過delete移除,而變量卻不可以。所以通過var關鍵字聲明的變量是不能用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
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。另外有一個特例,eval上下文里不會給變量添加{DontDelete}屬性,所以eval上下文里的變量可以被刪除。
__parent__ 屬性(略,因為現在在Chrome和Firefox里都不支持,看不見的東西,討論也沒有意義)
=============================================================其他的問題
問題一.有個人在提到一個關於var關鍵字的問題時,引起了我的一個想法,當我們在函數里使用a=10時,其實是效果是等同於window.a=10,前面已經解釋過了,我在想的是,這會不會是因為函數里的this指向的是window,也就是全局對象,因為所有演示這個問題的代碼使用的都是全局作用域里聲明的一個函數並且在隨後即調用。所以我在揣測,會不會其實原則是:在this指向的對象上創建沒有用var關鍵字的賦值操作,所以,因為在這些例子中this都是指向全局對象的,所以a才在window上被創建。
先通過下面代碼再次演示這個問題,並且把它作為參照組:注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
function foo() { a = 10; console.log(window.a); // 10 console.log(this); // → file:///D:/workcomplex/jslab/varobj/home.html console.log(window === this); // true console.log(this.a); // 10 console.log(a); // 10 } foo(); console.log(window.a); // 10
我設計了下面的代碼來更改一個函數執行時this指向的對象:
var myObj = {x:2, y:67}; function foo() { a = 10; console.log(window.a); // 10 console.log(this); // Object { x:2, y:67} console.log(this.a); // undefined console.log(a); // 10 } foo.call(myObj); console.log(window.a); // 10 console.log(myObj); // Object { x:2, y:67}
這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同。從結果來看,我的設想是錯誤的,不論this指向什麼,a=10這樣的代碼都會在window上創建a屬性。
問題二.
有人針對各種示例代碼中出現的一個現象問了個問題,如代碼:
alert(this.x); //undefined alert(x); //x is not defined
明明在全局對象上沒有定義x屬性,可是嘗試訪問它得到的是undefined,可是訪問沒有聲明的變量得到的卻是一個引用異常被拋出。關於這個問題,作者的解釋是,這就是標識符解析(identifier resolution)的規則,沒有太複雜的原因,在一個對象上找不到某個屬性就默認返回undefined,而找不到一個變量就拋出異常。
問題三.
有人發現了一個新的情況:
function myFunction(){ var x = 1; myFunction.y = 2; }
這種情況下這個y怎麼算?放在myFunction上下文的觸發體上?答案是y既然定義在myFunction上,那麼它就放在myFunction對象上,函數也是對象嘛,它跟觸發體不會發生直接關係。所以觸髮體的狀態是:
AO (myFunction) = { arguments: { "callee": <myFunction>, "length": 0 }, x: undefined }
問題四.
這個問題應該在後面說會好一些,它屬於作用域的問題。不過既然作者在評論里說了,那就先在這裡講一下。基本上大家都知道JavaScript沒有代碼塊作用域(block scope),很多面試題也是考這個點,比如說if-else代碼塊和for循環代碼塊里創建的變量,其實在這些聲明它們的區塊以外都能訪問到的。作者給了個例子,在Chrome (Version 56.0.2924.87)和Firefox
(51.0.1 (32-bit))中測試的結果都相同:
// global var foo = 10; if (true) { var foo = 20; alert(foo); // 20 } // block ends, but "foo" wasn't local alert(foo); // 20
但是,正如作者指出,有一個特別,就是catch代碼塊,傳給它的參數只會在catch區塊內有效,所以它的作用域是代碼塊級別的(在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同)。
var foo = 10; try { throw 20; } catch (foo) { alert(foo); // 20 } // again global alert(foo); // 10
相关文章推荐
- DS.Lab筆記 - ECMA-262-3: this
- DS.Lab筆記 - ECMA-262-3: 函数
- DS.Lab筆記 - ECMA-262-3: 闭包
- DS.Lab筆記 - ECMA-262-3: ECMAScript对于面向对象语言功能的实现
- DS.Lab筆記 - ECMA-262-5 - 属性与属性描述器
- DS.Lab筆記 - ECMA-262-3: 作用域链
- DS.Lab筆記 - ECMA-262-5 - 严谨模式
- DS.Lab筆記 - ECMA-262-3: 求值策略
- DS.Lab筆記 - ECMA-262-5 - 文法环境概论
- ECMA-262,第 5 版。最新 JavaScript 规范 了解 ECMAScript 规范的历史,查看它的众多重要新特性和新概念。
- [JavaScript]ECMA-262-3 深入解析.第四章.函数
- JavaScript函数代码和执行上下文--ECMA-262-5
- ECMA-262-5 in detail. Chapter 2. Strict Mode.
- [JavaScript]ECMA-262-3 深入解析.第五章.闭包
- [JavaScript]ECMA-262 深入解析
- [JavaScript]ECMA-262 深入解析
- V8引擎实现标准ECMA-262(三)
- [JavaScript]ECMA-262-3 深入解析.第一章.执行上下文
- Rt preempt Howto [dslab]
- ECMA262深入浅出[引用]