1. 程式人生 > >從ECMAScript規範深度分析JavaScript(二):變數物件(下)

從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