JS學習系列 06 - 變量對象

上一節咱們討論了執行上下文,那麼在上下文中到底有什麼內容,爲何它會和做用域鏈扯上關係,JS 解釋器又是怎麼找到咱們聲明的函數和變量,看完這一節,相信你們就不會再迷惑了。node

變量對象就是執行上下文做用域鏈中間的橋樑。
劇透一下,神祕的 this 就存在於執行上下文環境之中!
固然,以後我會單獨用幾節來完全講明白 this 究竟是什麼(其實 this 很簡單)。數組

接下來,咱們進入正文。瀏覽器

1. 執行上下文包含什麼

一個執行上下文咱們能夠抽象的理解爲對象(object)。
每個執行上下文都有一些屬性(又稱爲上下文狀態),它們用來追蹤關聯代碼的執行進度。微信

我用一個結構圖來講明:函數

執行上下文環境 object

Variable Object 就表明變量對象。
Scope Chain 表明做用域鏈。
thisValue 表明神祕的 this 。this

做用域鏈和 this 留到後面再講,今天咱們先來弄明白變量對象spa

2. 變量對象

A variable object is a scope of data related with the execution context. It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

變量對象(variable object) 是與執行上下文相關的數據做用域(scope of data) 。它是與上下文關聯的特殊對象,用於存儲被定義在上下文中的 變量(variables) 和 函數聲明(function declarations) 。code

變量對象(Variable Object -- 簡寫 VO)是一個抽象的概念,指代與執行上下文相關的特殊對象,它存儲着在上下文中聲明的:對象

  • 變量(var)
  • 函數聲明 (function declaration,簡寫 FD)
  • 函數的形參(arguments)

咱們假設變量對象爲一個普通 ECMAScript 對象:遞歸

VO = {};

就像前面講過的,VO 是執行上下文的一個屬性:

activeExecutionContext = {
  VO: {
    // 上下文數據 (vars, FD, arguments)
  }
}

由於變量對象是一個抽象的概念,因此並不能經過變量對象的名稱直接訪問,可是卻能夠經過別的方法來間接訪問變量對象,例如在全局上下文環境的變量對象會有一個屬性 window (DOM 中) 能夠引用變量對象自身,全局上下文環境的另外一個屬性 this 也指向全局上下文環境的變量對象。

舉個例子:

var a = 2;

function foo (num) {
   var b = 5;
}

(function exp () {
   console.log(111);
})

foo(10);

這裏對應的變量對象是:

// 全局上下文環境的變量對象
VO(globalContext) = {
   // 一些全局環境初始化時系統自動建立的屬性: Math、String、Date、parseInt等等
   ···

   // 全局上下文的變量對象中有一個屬性能夠訪問到自身,在瀏覽器中這個屬性是 window ,在 node 中這個屬性是 global
   window: global

   // 本身定義的屬性
   a: 10,
   foo: <reference to function>
};

// foo 函數上下文的變量對象
VO(foo functionContext) = {
   num: 10,
   b: 5
};

注意:函數表達式並不包括在變量對象中。

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

執行上下文包括:全局上下文、函數上下文和 eval() 上下文。

全局上下文中的變量對象

這裏咱們先來了解一下什麼是全局對象:

全局對象(global object)是指在進入任何執行上下文以前就已經建立了的對象。
這個對象只有一份,它的屬性在程序中的任何地方均可以訪問,全局對象的生命週期終止於程序退出的那一刻。

全局對象初始化時系統將建立並初始化一系列原始屬性,例如:Math、String、Date、parseInt、window等等,以後是咱們在全局上下文中本身定義的全局變量。在 DOM 中,全局對象的 window 屬性能夠引用全局對象自身,全局上下文環境的 this 屬性也能夠引用全局對象。

// 全局執行上下文環境
EC(globalContext) = {
   // 全局對象(全局上下文環境的變量對象) 
   global: {
      Math: <...>,
      String: <...>,
      ...
      ...
      window: global     // 引用全局對象自身
   },
   
   // this 屬性
   this: global

   // 做用域鏈
   ...
}

舉個例子:

var a = 10;

console.log(a);               // 10
console.log(window.a);        // 10
console.log(this.a);          // 10

所以,在全局上下文環境中,變量對象用全局對象來表示。

函數上下文中的變量對象

在函數上下文中,變量對象用活動對象 AO(Active Object)來表示。

VO(functionContext) = AO

活動對象是在進入函數上下文時刻被建立的,它是經過函數的 arguments 屬性進行初始化。arguments 也是一個對象。

AO = {
   arguments: {
      ...
   }
}

arguments 是活動對象的一個屬性,它也是一個對象,包括如下屬性:

  1. callee - 指向當前函數的引用
  2. length - 真正傳遞的參數個數
  3. properties-indexes - index 是字符串類型的整數,例如"1": "aa",相似於數組類型,也能夠經過arguments[1]來訪問,可是不能用數組的方法(push, pop等等)。另外,properties-indexes 的值和實際傳遞進來的參數之間是共享的,一個改變,另外一個也隨之改變。

舉個例子:

function foo (x, y, z) {
  
   // 聲明的函數參數數量
   console.log(foo.length);      // 3

   // 實際傳遞進來的參數數量
   console.log(arguments.length);      // 2

   // arguments 的 callee 屬性指向當前函數
   console.log(arguments.callee === foo)   // true

   // 參數共享
   console.log(x === arguments[0]);      // true
   console.log(x);      // 10

   arguments[0] = 20;
   console.log(x);   // 20

   x = 30;
   console.log(arguments[0]);    // 30

   // 可是注意,沒有傳遞進來的參數 z ,和第3個索引值是不共享的
   z = 40;
   console.log(arguments[2]);      // undefined

   arguments[2] = 50;
   console.log(z);      // 40
}

foo(10, 20);

4. 代碼是如何被處理的

在第1節中咱們講過js 代碼的編譯過程,其中有一步叫做預編譯,是說在代碼執行前的幾微秒會首先對代碼進行編譯,造成詞法做用域,而後執行。

那麼執行上下文的代碼就就能夠分紅兩個階段來處理:

  1. 進入執行上下文(預編譯)
  2. 執行代碼

而變量對象的修改變化和這兩個階段是緊密相關的。
而且全部類型的執行上下文都會有這2個階段。

進入執行上下文

當引擎進入執行上下文時(代碼還未執行),VO 裏已經包含了一些屬性:

  1. 函數的全部形參(若是是函數執行上下文)

由名稱和對應值組成的一個變量對象的屬性被建立,若是沒有傳遞對應的實參,那麼由名稱和 undefined 組成的一種變量對象的屬性也會被建立。

  1. 全部的函數聲明(Function Declaration - FD)

由名稱和對應值(函數對象 function object)組成的一個變量對象的屬性被建立,若是變量對象已經存在相同名稱函數的屬性,則徹底替換這個屬性。

  1. 全部的變量聲明(Variable Declaration - var)

由名稱和對應值(在預編譯階段全部變量值都是 undefined)組成的一個變量對象的屬性被建立,若是變量名和已經聲明的形參或者函數相同,則變量名不會干擾已經存在的這類屬性,若是已經存在相同的變量名,則跳過當前聲明的變量名。

注意:變量碰到相同名稱的變量是忽略,函數碰到相同名稱的函數是覆蓋。

舉個例子:

function foo (a, b) {
   var c = 5;

   function bar () {};

   var d = function _d () {};

   (function f () {});
}

foo(10);

當進入帶有實參10的 foo 函數上下文時(預編譯時,此時代碼尚未執行),AO 結構以下:

AO(foo) = {
   a: 10,
   b: undefined,

   c: undefined,
   bar: <reference to FunctionDelcaration "bar">,
   d: undefined 
};

注意,函數表達式 f 並不包含在活動對象 AO 內。
也就是說,只有函數聲明會被包含在變量對象 VO 裏面,函數表達式並不會影響變量對象。

行內函數表達式 _d 則只能在該函數內部可使用, 也不會包含在 VO 內。

這以後,就會進入第2個階段,代碼執行階段。

代碼執行

在這個階段,AO/VO 已經有了屬性(並非全部的屬性都有值,大部分屬性的值仍是系統默認的初始值 undefined)。

AO 在代碼執行階段被修改以下:

AO['c'] = 5;
AO['d'] = <reference to FunctionDelcaration "_d">

再次要提醒你們,由於函數表達式 _d 已經保存到了聲明的變量 d 上面,因此變量 d 仍然存在於 VO/AO 中。咱們能夠通 d() 來執行函數。可是函數表達式 f 卻不存在於 VO/AO 中,也就是說,若是咱們想嘗試調用 f 函數,無論在函數定義前仍是定義後,都會出現一個錯誤"f is not defined",未保存的函數表達式只有在它本身的定義或遞歸中才能被調用。

再來一個經典例子:

console.log(x);      // function

var x = 10;
console.log(x);      // 10

x = 20;

function x () {};

console.log(x);      // 20

這裏爲何是這樣的結果呢?

上邊咱們說過,在代碼執行以前的預編譯,會爲變量對象生成一些屬性,先是形參,再是函數聲明,最後是變量,而且變量並不會影響同名的函數聲明。

因此,在進入執行上下文時,AO/VO 結構以下:

AO = {
   x: <reference to FunctionDeclaration "x">

   // 在碰到變量聲明 x 時,由於已經存在了函數聲明 x ,因此會忽略
}

緊接着,在代碼執行階段,AO/VO 被修改以下:

AO['x'] = 10;
AO['x'] = 20;

但願你們能夠好好理解變量對象,對於理解咱們後邊要講的做用域鏈有很大的幫助。

5. 變量

有一些文章說過:

不論是使用 var 關鍵字(在全局上下文)仍是不使用 var 關鍵字(在任何地方),均可以聲明一個變量。

請記住,這是錯誤的觀念。

任什麼時候候,變量都只能經過使用 var 關鍵字來聲明(ES6 以前)

a = 10;

上面的賦值語句,僅僅是給全局對象建立了一個新屬性(在在非嚴格模式,嚴格模式下會報錯),但注意,它不是變量。「不是變量」並非說它不能被改變,而是指它不符合ECMAScript 規範中變量的概念。

讓咱們經過一個例子來看一下二者的區別:

console.log(a);        // undefined
console.log(b);        // 報錯,b is not defined

b = 10;
var a = 20;

只要咱們很好的理解了:變量對象、預編譯階段和執行代碼階段,就能夠迅速的給出答案。

預編譯(進入上下文)階段:

VO = {
   a: undefined
}

咱們能夠看到,由於 b 不是經過 var 聲明的,因此這個階段根本就沒有 b ,b 只有在代碼執行階段纔會出現。可是在這個例子中,尚未執行到 b 那就已經報錯了。

咱們稍微更改一下示例代碼:

console.log(a);      // undefined

b = 10;
console.log(b);             // 10 代碼執行階段被建立
console.log(window.b);      // 10
console.log(this.b);        // 10

var a = 20;
console.log(a);      // 20 代碼執行階段被修改

關於變量,還有一個很重要的知識點。

變量不能用 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);    // 20

注意:這個規則在 eval() 上下文中不起做用。

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

console.log(delete a);    // true

console.log(window.a);    // undefined

6. 總結

這一節中咱們講了變量對象,下一節就是咱們的重頭戲 - 做用域鏈。但願你們能夠持續關注我,咱們一塊兒進步。

歡迎關注個人公衆號

微信公衆號

相關文章
相關標籤/搜索