從ECMAScript規範深度分析JavaScript(二):變數物件(下)
本文譯自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中會加入一些個人見解以及配圖舉例等等,來幫助讀者更好的理解JavaScript。
宣告:本文不涉及與ES6相關的知識。
前言
在本系列教程上一篇文章《從ECMAScript規範深度分析JavaScript(二):變數物件(上)》中我們講述了變數物件的概念,以及在不同上下文中變數物件的區別,接下來讓我們來深入探討一下,在程式的不同階段變數物件的行為、關於變數所要知道的細節以及特殊屬性__parent__。
處理上下文程式碼的階段
現在我們終於觸及到本文的核心內容,執行上下文的程式碼被分成兩個基本的階段來處理:
- 進入執行期上下文
- 程式碼執行
變數物件的變化與這兩個階段緊密相關。
注意:這兩個階段不管是全域性還是函式上下文的共同行為。
1、進入執行期上下文
在進入執行期上下文還沒開始執行程式碼時,VO中有以下屬性(在之前已經闡述過了):
- 所有的形參(如果是在函式的執行期上下文中)
這是變數物件的一個由形參的名和值建立的屬性;如果沒有對應傳遞實際引數,那麼這個屬性就由形式引數的名稱和undefined值建立 - 所有的函式宣告
這是變數物件的一個由函式物件的名和值建立的屬性;如果變數物件已經包含了一個相同的屬性名稱,那麼將會替換掉他的值和特性 - 所有的變數宣告
這是變數物件的一個由變數名和值建立的一個屬性;如果變數名已經和一個形參或者是一個函式名相同,則變數宣告不會改變已經存在的屬性
我們來看個例子:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // 呼叫
在傳遞10這個引數進入test函式上下文時,AO像下面這樣
AO(test) = { a: 10, b: undefined, c: undefined, d: <reference to FunctionDeclaration "d"> e: undefined };
注意:AO並不包含函式x,這是因為x不是函式宣告而是函式表示式(Function-Expression,縮寫為FE),而函式表示式不會影響變數物件。
然而,函式_e也是一個函式表示式,但是我們接下來會發現,因為將它賦值給了變數e,我們可以通過變數名e來訪問它(我們暫時先不討論函式宣告和函式表示式的區別)。
在這之後,就來到了上下文程式碼進行的第二階段,程式碼執行期。
2、程式碼執行
此時,AO/VO已經充滿了屬性(但是,它們不是所有的屬性都有真實值,其中的大多數仍然是初始值undefined)。
思考一下同樣的例子,AO/VO在程式碼解釋執行期間被修改:
AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;
我們再次注意到,函式表示式_e仍然只存在於記憶體中,因為它被儲存給了變數宣告e,但是函式表示式x不存在於AO/VO中。如果我們嘗試在定義前(甚至是定以後)呼叫x函式,我們將會得到一個錯誤:“x” is not defined。
注意:沒有儲存的函式表示式只能在他定義的地方或者遞迴中呼叫。
一個經典的例子:
alert(x); // 是個函式
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20
為什麼第一次列印x是函式並且還是在定義它之前訪問的?為什麼不是10或者20呢?
因為: 根據規則,VO物件在進入上下文的階段填入函式聲明瞭(這個常被稱為函式宣告提前);在同一階段,還有一個變數宣告“x”,那麼正如我們之前提及的那樣,變數宣告在順序上跟在函式宣告和形式引數宣告之後,而且,在這個階段(指進入執行上下文階段),變數宣告不會干擾VO中已經存在的同名函式宣告或形式引數宣告,因此,在進入上下文時,VO的結構如下:
VO = {};
VO['x'] = <reference to FunctionDeclaration "x">
// 發現var x = 10;如果函式x不是已經定義了,x將會是undefined,但在我們的示例中,變數宣告沒有影響到具有相同名稱函式的值。
VO['x'] = <the value is not disturbed, still function>
之後,我們進入到程式碼執行階段,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, 而不是"b is not defined",證明b是宣告過了的
注:這是因為在ES6之前,javascript沒有塊級作用域的概念,ES6中情況會有其他情況(比如let宣告),我們在這裡不作討論。
關於變數
通常,各類文章甚至JavaScript書籍都聲稱:“可以在全域性作用域中使用var關鍵字或者在所有地方不使用var關鍵字來宣告全域性變數”。事實上並不是這樣,一定要記住:
任何時候,變數只能通過使用var關鍵字才能宣告。
向下面這樣賦值:
a = 10;
只是建立了全域性物件的一個屬性,而不是變數。“不是變數”不是說它不能夠被改變,而是它不符合ECMAScript的概念(他們也變成了全域性物件的屬性,因為 VO(globalContext) === global,這個我們在之前就有提及)。
我們來看一下這個例子來了解他們的區別:
alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;
所有這些都取決於VO和他的修改階段(進入上下文階段和程式碼執行階段)
進入上下文時:
VO = {
a: undefined
};
我們發現在這個階段沒有b,因為它不是變數,b將只會在程式碼執行階段出現(但是在我們的程式碼中沒有,因為會有報錯)
改一下程式碼:
alert(a); // undefined, 我們知道這種情況
b = 10;
alert(b); // 10, 在程式碼執行階段被建立
var a = 20;
alert(a); // 20, 在程式碼執行階段被修改
這裡關於變數還有一個更重要的點,和普通簡單屬性不同,變數有{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
所以有些人使用某些debug除錯工具的控制檯測試這個例子時會出現問題,比如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中訪問啟用物件:根據版本的不同,內部函式的__parent__屬性返回null或者全域性物件。
在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
})();
結語
在本章中我們進一步學習了和執行期上下文相關的物件,此篇是很重要的一章,明白了這其中的機制,我們才能夠對後續的作用域鏈,閉包等知識有更好的理解。
希望此文能夠解決大家工作和學習中的一些疑問,避免不必要的時間浪費,有不嚴謹的地方,也請大家批評指正,共同進步!
轉載請註明出處,謝謝!
交流方式:QQ1670765991