JavaScript內部原理系列-變量對象(Variable object)

概要

咱們老是會在程序中定義一些函數和變量,以後會使用這些函數和變量來構建咱們的系統。
然而,對於解釋器來講,它又是如何以及從哪裏找到這些數據的(函數,變量)?當引用一個對象的時候,在解釋器內部又發生了什麼?html

許多ECMA腳本程序員都知道,變量和執行上下文是密切相關的:前端

var a = 10; // 全局上下文中的變量

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

alert(a); // 10
alert(b); // "b" is not defined
</pre>

不只如此,許多程序員也都知道,ECMAScript標準中指出獨立的做用域只有經過「函數代碼」(可執行代碼類型中的一種)才能建立出來。比方說,與C/C++不一樣的是,在ECMAScript中for循環的代碼塊是沒法建立本地上下文的:程序員

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

alert(k); // 儘管循環已經結束,可是變量「k」仍然在做用域中

下面就來詳細介紹下,當聲明變量和函數的時候,究竟發生了什麼。瀏覽器

數據聲明

既然變量和執行上下文有關,那它就該知道數據存儲在哪裏以及如何獲取。這種機制就稱做變量對象:ecmascript

A variable object (in abbreviated form — VO) is a special object related with an execution context and which stores:
variables (var, VariableDeclaration);
function declarations (FunctionDeclaration, in abbreviated form FD);
and function formal parameters
declared in the context.ide

舉個例子,能夠用ECMAScript的對象來表示變量對象:函數

<pre>VO = {};
</pre>

VO同時也是一個執行上下文的屬性:工具

<pre>activeExecutionContext = {
  VO: {
    // 上下文中的數據 (變量聲明(var), 函數聲明(FD), 函數形參(function arguments))
  }
};

對變量的間接引用(經過VO的屬性名)只容許發生在全局上下文中的變量對象上(全局對象自己就是變量對象,這部分會在後續做相應的介紹)。 對於其餘的上下文而言,是沒法直接引用VO的,由於VO是實現層的。post

聲明新的變量和函數的過程其實就是在VO中建立新的和變量以及函數名對應的屬性和屬性值的過程。測試

以下所示:

var a = 10;

function test(x) {
  var b = 20;
};

test(30);

上述代碼對應的變量對象則以下所示:

// 全局上下文中的變量對象
VO(globalContext) = {
  a: 10,
  test:
};

// 「test」函數上下文中的變量對象
VO(test functionContext) = {
  x: 30,
  b: 20
}; 可是,在實現層(標準中定義的),變量對象只是一個抽象的概念。在實際執行上下文中,VO可能徹底不叫VO,而且初始的結構也可能徹底不一樣。

不一樣執行上下文中的變量對象

變量對象上的一些操做(好比:變量的初始化)和行爲對於全部的執行上下文類型來講都已同樣的。從這一點來講,將變量對象表示成抽象的概念更加合適。 函數上下文還能定義額外的與變量對象相關的信息。

AbstractVO (generic behavior of the variable instantiation process)

  ║
  ╠══> GlobalContextVO
  ║        (VO === this === global)
  ║
  ╚══> FunctionContextVO
           (VO === AO,  object and  are added)

接下來對這塊內容進行詳細介紹。

全局上下文中的變量對象

首先,有必要對全局對象(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); // directly, is found in VO(globalContext): "test"

alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true

var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"

函數上下文中的變量對象

在函數的執行上下文中,VO是不能直接訪問的。它主要扮演被稱做活躍對象(activation object)(簡稱:AO)的角色。

VO(functionContext) === AO;

活躍對象會在進入函數上下文的時候建立出來,初始化的時候會建立一個arguments屬性,其值就是Arguments對象:

AO = {
  arguments:
};

Arguments對象是活躍對象上的屬性,它包含了以下屬性:

  • callee —— 對當前函數的引用
  • length —— 實參的個數
  • properties-indexes(數字,轉換成字符串)其值是函數參數的值(參數列表中,從左到右)。properties-indexes的個數 == arguments.length;
    arguments對象的properties-indexes的值和當前(實際傳遞的)形參是共享的。

以下所示:

function foo(x, y, z) {

  // 定義的函數參數(x,y,z)的個數
  alert(foo.length); // 3

  // 實際傳遞的參數個數
  alert(arguments.length); // 2

  // 引用函數自身
  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,
  // 相關的arguments對象的index-property是不共享的

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

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

}

foo(10, 20);

上述例子,在當前的Google Chrome瀏覽器中有個bug——參數z和arguments[2]也是互相共享的。

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

至此,也就到了本文最核心的部分了。處理執行上下文代碼分爲兩個階段:

  1. 進入執行上下文
  2. 執行代碼

對變量對象的修改和這兩個階段密切相關。

要注意的是,這兩個處理階段是通用的行爲,與上下文類型無關(不論是全局上下文仍是函數上下文都是一致的)。

進入執行上下文

一旦進入執行上下文(在執行代碼以前),VO就會被一些屬性填充(在此前已經描述過了):

  • 函數的形參(當進入函數執行上下文時)
    —— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對於沒有傳遞的參數,其值爲undefined
  • 函數聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數對象建立出來的;若是變量對象已經包含了相同名字的屬性,則替換它的值
  • 變量聲明(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: 
  e: undefined
};

注意了,上面的AO並不包含函數「x」。這是由於這裏的「x」並非函數聲明而是函數表達式(FunctionExpression,簡稱FE),函數表達式不會對VO形成影響。 儘管函數「_e」也是函數表達式,然而,正如咱們所看到的,因爲它被賦值給了變量「e」,所以它能夠經過「e」來訪問到。關於函數聲明和函數表達式的區別會在第五章——函數做具體介紹。

至此,處理上下文代碼的第一階段介紹完了,接下來介紹第二階段——執行代碼階段。

執行代碼

此時,AO/VO的屬性已經填充好了。(儘管,大部分屬性都尚未賦予真正的值,都只是初始化時候的undefined值)。

繼續以上一例子爲例,到了執行代碼階段,AO/VO就會修改爲爲以下形式:

AO['c'] = 10;
AO['e'] = ;

再次注意到,這裏函數表達式「_e」仍在內存中,這是由於它被保存在聲明的變量「e」中,而一樣是函數表達式的「x」卻不在AO/VO中: 若是嘗試在定義前或者定義後調用「x」函數,這時會發生「x爲定義」的錯誤。未保存的函數表達式只有在定義或者遞歸時才能調用。

以下是更加典型的例子:

alert(x); // function

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

x = 20;

function x() {};

alert(x); // 20

上述例子中,爲什麼「x」打印出來是函數呢?爲什麼在聲明前就能夠訪問到?又爲什麼不是10或者20呢?緣由在於,根據規則——在進入上下文的時候,VO會被填充函數聲明; 同一階段,還有變量聲明「x」,可是,正如此前提到的,變量聲明是在函數聲明和函數形參以後,而且,變量聲明不會對已經存在的一樣名字的函數聲明和函數形參發生衝突, 所以,在進入上下文的階段,VO填充爲以下形式:

VO = {};

VO['x'] = 

// 發現var x = 10;
// 若是函數「x」還未定義
// 則 "x" 爲undefined, 可是,在咱們的例子中
// 變量聲明並不會影響同名的函數值

VO['x'] = 

隨後,在執行代碼階段,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, but not "b is not defined"

關於變量

大多數講JavaScript的文章甚至是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的修改(進入上下文階段和執行代碼階段):
進入上下文:

VO = {
  a: undefined
};

咱們看到,這個階段並無任何「b」,由於它不是變量,「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

這裏關於變量還有很是重要的一點:與簡單屬性不一樣的是,變量是不能刪除的{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」執行上下文中,是能夠刪除變量的:

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

alert(delete a); // true

alert(window.a); // undefined

利用某些debug工具,在終端測試過這些例子的童鞋要注意了:其中Firebug也是使用了eval來執行終端的代碼。所以,這個時候var也是能夠刪除的。

實現層的特性: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中,以相同的方式獲取活躍對象是容許的:

以下所示(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

})();

總結

本文,咱們介紹了與執行上下文相關的對象。但願,本文可以對你們有所幫助,同時也但願本文可以起到解惑的做用。

擴展閱讀

此文譯自Dmitry A.Soshnikov的文章Variable object
趙靜/宋珍珍 譯
via 前端翻譯小站

相關文章
相關標籤/搜索