JavaScript核心原理(一)執行環境、執行環境棧、變量對象、活動對象

前言


之前在閱讀《Javascript高級程序設計》「4.2執行環境及作用域的」時候,對相關的概念理解得並不是非常的透徹,只是懂了大概的意思。後來在看到「閉包」這一節時書中再一次提到了相關的概念,並且這些是充分理解閉包的必要背景知識,於是這一次我不能再略讀了,必須徹徹底底地弄明白。啃了兩天的相關文章、資料後,算是有一個比較清晰的認識了,現在記錄下來,希望可以幫到同樣對相關概念不熟悉的同學,也可以用作自己日後的回顧和修正。

注:Execution Context 可以被翻譯爲「執行上下文」或者「 執行環境」,文中可能都會用到,大家記住是一個東西就可以了。



什麼是執行環境(Execution Context)?

“每當程序的執行流進入到一個可執行的代碼時,就進入到了一個執行環境中。”

執行環境是 ECMA-262 中用以區分不同的可執行代碼的抽象概念
可執行代碼的類型可以爲分爲:

  • 全局代碼:程序載入後的默認環境,是運行在程序級別的代碼。
  • 函數代碼:當執行流進入一個函數後。
  • Eval代碼:Eval 內部的代碼。

#

執行環境棧(Execution Stack)

執行流依次進入的執行環境在邏輯上形成了一個棧,棧的底部永遠是全局環境,棧的頂部則是處於活動狀態當前的執行環境(瀏覽器總是執行處於棧頂的上下文)。當執行流進入一個函數時,函數的環境就會被推入這個環境棧中,當函數執行完畢之後,棧將這個執行環境彈出,然後把控制權返回給之前的執行環境。這樣實現的原因是由於 Javascript 解釋器是單線程的,也就是同一時刻只能發生一件事情,其他等待執行的上下文或事件就會在這個環境棧中排隊等待。值得注意的一點是:每次函數的調用都會創建一個執行環境壓入棧中,無論是函數內部的函數、還是遞歸調用等。


我們用數組來表示執行環境棧:

ECStack = [];

來看下面這個例子:

(function foo(i){
  if(i === 3){
    return console.log("Well,the current FunctionContext is finished.");
  }
  else{
    foo(++i);
  }
})(1);

這個函數會被調用3次,分別是 i = 1,i = 2,i = 3 的時候,每次被調用的時候都會創建一個執行上下文然後壓入棧中,執行完畢之後再被彈出,最後將控制權交給棧底的全局環境。當第三次調用 foo 函數也就是 i = 3 時,ECStack 狀態如下:

ECStack =
[
  //棧頂
  FunctionContext - foo(3);
  FunctionContext - foo(2);
  FunctionContext - foo(1);
  GlobalContext
  //棧底
]


變量對象 (Variable Object)

每一個執行環境都有一個與之相關的變量對象,其中存儲着上下文中聲明的:

  • 變量 VariableDeclaration VD
  • 函數 FunctionDeclaration FD
  • 形式參數 formal parameters

我們可以用一個對象來表示變量對象:

VO = {
  // 執行上下文中聲明的變量、函數、形式參數
}


不同執行環境下的變量對象

變量對象是一個抽象的概念,在進入具體的執行上下文時,變量對象在具體實現上也會有相應地差別。

AbstractVO (generic behavior of the variable instantiation process)

╠══> GlobalContextVO
(VO === this === global)
╚══> FunctionContextVO

(VO === AO, <arguments> object and <formal parameters> are added)

全局上下文中的變量對象

全局對象是一個在進入任何執行上下文前就創建出來的對象;此對象以單例形式存在;它的屬性在程序任何地方都可以直接訪問,其生命週期隨着程序的結束而終止。

全局對象的屬性在任何地方都可以被訪問到,可以通過 this 或者 DOM 中的 Window 對象來訪問。全局對象中的變量對象就是全局對象本身,理解這一點很重要,正是因爲這個原因才使得可以通過全局對象的屬性來訪問在全局上下文中聲明的變量。

函數上下文中的變量對象

當函數被調用時,一個特殊的對象——活動對象就隨之創建了。變量對象通過函數的 arguments 對象來初始化,arguments 對象是活動對象上的屬性,包含了以下屬性:

  • callee 對當前函數的引用
  • length 傳入的實參個數
  • properties-indexes 參數對應的索引值,相應的值和實際傳入的參數值是共享的,但不併是存儲在同一個地方的

執行環境的具體細節


我們同樣也可以用一個對象來表示執行上下文:

ExecutionContextObj = {
    scopeChain: { 變量對象(variableObject)+ 所有父執行上下文的變量對象 },
    variableObject: { <arguments>對象,內部變量聲明和函數聲明 },
    this:{}
}

每當一個函數被調用的時候,就會隨之創建一個執行上下文,在 Javascript 解釋器內部處理執行上下文有兩個步驟:
  • 第一步:創建階段 (在函數調用之後,函數體執行之前),解釋器掃描傳遞給函數的參數或arguments,本地函數聲明和本地變量聲明,並創建executionContextObj對象。掃描的結果將完成變量對象的創建
    *創建作用域鏈 (Scope Chain)
    • 掃描上下文中聲明的形式參數、函數以及變量,並依次填充變量對象的屬性

    • 函數的形參:形參作爲屬性,對應的實參作爲值。對於沒有實參的形參,值爲undefined。

    • 函數聲明(FunctionDeclaration FD):由函數對象創建出相應的名、值,名就是函數名、值就是函數體。如果變量對象已經包含了同名的屬性,就會替換掉它的值。
    • 變量聲明(VariableDeclaration):屬性名是變量名,值初始化爲 undefined。如果變量名和已經存在的屬性同名,不會影響到同名的屬性。
    • 注意:函數表達式(FunctionExpression FE)不會成爲變量對象的屬性,也就是說函數表達式不會影響到變量對象。

    • 求出上下文“this”的值

  • 第二步:代碼執行階段

    • 這一階段就會給第一步中初始值爲 undefined 的變量賦上相應的值
      我們來看下面這個例子:
(function foo(x,y,z){

  var a = 1;
  var b = function(){};
  function c(){}
  (function d(){})();

})(10,20);

函數調用後,相應的executionContextObj如下:

第一階段

executionContextObj = {
    scopeChain:{...},
    VO: {
        arguments:{
            x:10,
            y:20,
            Z:undefined,
            length:2,//這裏是實際傳入參數的個數
            callee:pointer to function foo()
        }
        a:undefined,
        b:undefined,
        c:pointer to function c()
    },
    this:{...}
}

第二階段:

executionContextObj = {
        scopeChain:{...},
  VO: {
          arguments:{
              x:10,
              y:20,
              Z:undefined,
              length:2,//這裏是實際傳入參數的個數
                    callee:pointer to function foo()
            }
              a:1,
              b:pointer to function b(),
              c:pointer to function c()
      },
        this:{...}
}


在第二階段,就會爲局部變量 a 、b 賦值,注意到 d 並沒有在變量對象中,正如上文中提到的那樣,函數表達式是不會影響變量對象的,所以在作用域中任何一個位置引用d都會出現“d is not defined”的錯誤。

現在你應該非常清楚JS中的變量、函數聲明提升是怎麼回事了吧。


舉個例子吧:

(function foo(){

  console.log(typeof x);//"function"
  var x = 10;
  console.log(y);//undefined 而不是 “y is not defined” ,這就是變量聲明提升!
  var y = 20;
  console.log(typeof x);//"number"
  function x(){}

})();


爲什麼第一次打印x的類型是函數,第二次打印x的類型又是數字呢。這是因爲,根據創建上下文時的規則,函數調用之後會按照順序依次把函數參數、函數聲明、變量聲明填充爲VO的屬性,並且填充變量聲明的時候如果同名是不會造成任何影響的,x的值還是函數。

在進入上下文階段,VO的狀態:

VO = {
  x:pointer to function x()
}
//發現var x = 10;
//如果函數“x”還未定義,則 "x" 的值爲undefined,
//但是,在這個例子中
//變量聲明並不會影響同名的值爲函數的x


VO[‘x’] 的值仍未改變
在代碼執行階段,VO的狀態:

VO['x'] = 10;

這一階段,局部變量 x 被賦值,此時之前同名的值爲函數的 x 就會被覆蓋,大家注意聲明和賦值!!第一階段,局部變量聲明同名不會影響;第二階段局部變量賦值就會產生影響了,畢竟人家是最後賦值的嘛。

最後,再來說說關於變量聲明的問題:

在《Javascript高級程序設計》4.2.2一節當中有這麼一句話:“如果初始化變量時沒有使用var聲明,該變量會自動被添加到全局環境中。” 首先,我們應該先明確一點,使用var關鍵字是聲明變量的唯一方式。如果沒有var 的話,例如 a = 5 ,a 就將作爲全局對象的一個屬性,而不是一個變量。


區別如下:

alert(x); //"x" is not defined
alert(b); //"undefined

x = 10;
var y = 20;


進入上下文後第一階段:

VO = {
x:10;
}

VO 中並沒有y的原因是,y 並不是變量。另外還要注意的一點就是,沒有通過 var 聲明的屬性可以通過delete操作符刪除,而通過var聲明的變量就不可以。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章