ECMA-262 Javascript 核心
YehYeh\'s Notepad yehyeh@gmail.com 

ECMA-262 Javascript核心

本文翻譯自Dmitry Soshnikov 的部落格
採重點式翻譯,並非依順序逐字翻譯,另有刪除、補充一些內容,若有疑惑請參考原文。

物件(An Object)

  • 物件是屬性的集合,並且擁有一個prototype物件。
  • prototype可以也是一個物件或是null
  • 物件可以用[[prototype]]屬性,參考到的prototype
  • 在大部份的瀏覽器引擎中,常用__proto__來代替[[prototype]]
    • foo物件有2個屬性x, y,以及一個隱藏的屬性__proto__
    • __proto__指向foo的原型物件(prototype)
      var foo = {
          x : 10,
          y : 20
      };
      
    Figure 1. A basic object with a prototype.
Δ 回到最上方

原型鏈(A Prototype Chain)

  • 原型鏈(Prototype Chain)
    • ECMAScript中規定Object.prototype的__proto__屬性為null
    • 如果一個物件沒有明確指定prototype,則預設__proto__Object.prototype
    • Prototype物件也擁有__proto__屬性,參考到自己的Prototype物件
      補充圖1 - 原型(Prototype)
    • 如果prototype的__proto__不是空指標,則稱為原型鏈(Prototype Chain)
    • Prototype Chain是由有限數量的物件串連而成,可以用來實作繼承和屬性共用
    • Object.prototype本身的__proto__指向null,使prototype chain的串連中止
      補充圖 2- 原型鏈(Prototype Chain)
  • 程式碼重複使用(Code Reuse)
    • 若2個物件屬性大都相同,只有些微的差異,我們希望共用相同部份的程式碼,而不是每個物件重複一次。
    • ECMAScript可以使用Prototype Chain來達到程式碼重複使用(Code Reuse)
    • 這種類似繼承的能力,叫作以委派為基礎的繼承(delegation based inheritance)或以原型為基礎的繼承(prototype based inheritance)。
  • 如果物件本身找不到屬性或方法,則嘗試到prototype chain中尋找,一層一層找下去,直到找到第一個同名的屬性或方法。若整個prototype chain都找不到,則回傳undefined。
  • b, c 透過prototype chain呼叫定義在a中的方法。
    var a = {
      x: 10,
      calculate: function (z) {
        return this.x + this.y + z
      }
    };
     
    var b = {
      y: 20,
      __proto__: a
    };
     
    var c = {
      y: 30,
      __proto__: a
    };
     
    // 呼叫繼承的方法
    b.calculate(30); // 60
    c.calculate(40); // 80
    
    Figure 2. A prototype chain.
  • this在prototype中是指原來的物件,而不是定義方法的物件(prototype)
    • b.calcuate( 30 )
    • this.x + this.y + z a.x=10, b.y=20, z=30
Δ 回到最上方

建構函式(Constructor)

  • 建構函式會用指定樣式建立物件,也會自動指派新物件的prototype
  • 該Prototype物件即建構函式的prototype屬性(ConstructorFunction.prototype)
  • 物件和函式都有__proto__屬性, 函式都有prototype屬性
  • 將上一個範例用建構函式重寫,並用Foo.prototype取代上例的a物件
    // 建構函式
    // 可以用相同的樣式建立物件,並各自擁有獨立的y屬性
    function Foo(y) {	
      this.y = y;	  
    }
     
    // 因為新建物件的__proto__會參考到Foo.prototype
    // 所以可以將要共用或繼承的屬性或方法定義在Foo.prototype中 
    
    // 定義要被繼承的屬性 "x"
    Foo.prototype.x = 10;
     
    // 定義要被繼承的方法 "calculate"
    Foo.prototype.calculate = function (z) {
      return this.x + this.y + z;
    };
     
    // 使用Foo建立b和c物件
    var b = new Foo(20);
    var c = new Foo(30);
     
    // 呼叫繼承的方法
    b.calculate(30); // 60
    c.calculate(40); // 80
     
    // 顯示參考到的屬性 
    console.log( 
      b.__proto__ === Foo.prototype, 		// true
      c.__proto__ === Foo.prototype, 		// true
    					  
      // Foo.prototype也會自動建立一個參考到建構函式自身的特殊的屬性constructor
      // Foo的實體b, c可以經由delegation來找到它,並檢查其建構函式 
      b.constructor === Foo, 				// true
      c.constructor === Foo, 				// true
      Foo.prototype.constructor === Foo, 	// true
     
      b.calculate === b.__proto__.calculate, // true
      b.__proto__.calculate === Foo.prototype.calculate // true					 
    );
    
    Figure 3. A constructor and objects relationship.
    • 上圖顯示,每個物件都有prototype
    • 建構函式Foo也有__proto__指向Function.prototype
    • Function.prototype也有__proto__指向Object.prototype
    • Foo.prototype也是Foo的屬性,參考到b和c的prototype物件
    • 如果把這個範例想像成類別語言,則建構函式和propotype合起來就相當於一個類別
    • 補充:Chrome中執行時的截圖


Δ 回到最上方

執行情境堆疊(Execution Context Stack)

  • 補充:
    • 執行情境(Execution Context)可以想成是一個物件,存放Javascript執行引擎處理某段程式碼時所需的變數、作用域、this值
  • ECMAScript的程式碼可以分成三種型態:全域程式函式程式eval程式
    • 全域程式:只會有一個實體
    • 函式程式:每次呼叫一個函式,都會進入函式執行情境,並演算函式的程式碼,可能有多個實體
    • eval程式:每次呼叫eval函式,進入eval執行情境,並演算函式的程式碼,可能有多個實體
  • ECMAScript的每段程式碼都在執行情境(Execution Context)中演算
    • 每次呼叫函式時(即使是函式遞迴呼叫自己)都會產生一個擁有新情境狀態的新情境
      一個函式可能產生無限組情境
  • 每呼叫一次函式,產生一個新的情境
    function foo(bar) {}
     
    // 呼叫同一個函式
    // 每次呼叫時都會產生
    // 具有不同情境狀態(如bar的引數)的新的情境					 
    foo(10);
    foo(20);
    foo(30);
    
  • 邏輯上,執行情境被實作成像堆疊一樣,因而稱作執行情境堆疊(Execution Context Stack)
  • 執行情境可能激活其它情境,例如一個函式呼叫另一個函式(或全域情境呼叫全域函式)
    • 激活(呼叫)另一個情境的情境被叫做呼叫者(Caller)
    • 被激活的情境叫被呼叫者(Callee)
    • 全域情境呼叫一個函式,該函式又呼叫內部函式。
    • 呼叫者(Caller)呼叫一個被呼叫者(Callee)呼叫者(Caller)暫停它的執行,並將控制權交給被呼叫者(Callee)
    • 被呼叫者(Callee)被放到堆疊頂端,成為目前執行中的執行情境
    • 被呼叫者(Callee)執行結束時,會將控制權還給呼叫者(Caller),並繼續執行呼叫者(Caller)的情境,直到結束
    • 被呼叫者(Callee)可以用return或透過exception離開
    • 若exception未被處理,可能導致結束一個或多個情境
  • 所有ECMAScript程式執行時期(Program Runtime)可以被表示成一個執行情境堆疊(Execution Context Stack)
  • 堆疊頂端是正在執行的情境(Active Execution Context, Active EC)
    Figure 4. An execution context stack.
  • ECMAScript程式執行的機制
    • 程式開始時會進入全域執行情境(Global Execution Context, Global EC)
      • 全域執行情境是第一個進入堆疊中的元素
      • 全域執行情境一定是堆疊最底端的元素
    • 接著全域程式進行初始化,建立必要的物件和函式。
    • 全域情境的執行期間,可能會執行其它己建立好的函式
      • 進入其執行情境,將新元素堆入堆疊。
    • 初始化完成後,執行系統(runtime system)會等待事件的觸發(如滑鼠點擊)
      • 事件觸發時會執行特定的函式,並進入新的執行情境
    • 下圖中,有EC1和Global EC兩個情境,當EC1進入和離開時,EC Stack的異動如圖:
      Figure 5. An execution context stack changes.
  • 堆疊中的每個執行情境可以被表示成一個物件,接著來看一個執行情境應該擁有的結構和狀態(屬性)
Δ 回到最上方

執行情境(Execution Context)

  • 可以用一個簡單的物件來表示一個執行情境
  • 每個執行情境都有一組屬性(情境狀態),可以用來追蹤相關程式碼的執行進度。
  • 執行情境的結構:
    Figure 6. An execution context structure.
  • 除了這三個必要的屬性(變數物件Scope Chainthis value),一個執行情境依據實作可能會含有不同的額外狀態
  • 接著會詳細討論情境的這些重要屬性
Δ 回到最上方

變數物件(Variable Object, VO)

  • 變數物件(Variable Object, VO)
    • 變數物件(VO)是一個和情境相關的特殊的物件
    • VO是執行情境相關的資料的作用域(Scope of data)
    • VO存放情境中定義的變數函式宣告
      • 函式表示式不會被包含在VO
    • VO是抽象概念,在不同形態的情境時,使用不同的物件來代表變數物件
      • 在全域情境時,VO是Global Object本身
        • 所以可以用全域物件的屬性名稱來參考到全域變數
    • 使用函式表示式宣告的baz函式,不含在變數物件中
      所以在函式外面參考baz時會產生參考錯誤(Reference Error)
    var foo = 10;
     
    // 函式宣告(function declaration, FD) 
    function bar() {} 		
    
    // 函式表示式(function expression, FE)
    (function baz() {}); 	
     
    console.log(
      this.foo == foo, 	  // true
      window.bar == bar	  // true
    );
     
    console.log(baz);        // ReferenceError, 尚未定義 "baz"
    
    Figure 7. The global variable object.
  • 在ECMAScript中,只有函式擁有一個新的作用域(Scope)
    • 在函式作用域中定義的變數和內部函式
      • 對函式外部而言是不可見的
      • 不會被放到全域變數物件中
    • 補充範例,for和if沒有作用域的,宣告在其中的變數,離開後能可存取
      • 變數i的作用範圍在foo函式,而非for迴圈
      • 變數j的作用範圍在foo函式,而非if區塊
      • foo的範圍外,無法存取i、j
      (function foo(){							
          for(var i = 0; i < 10; i++){
              if( i == 5)
                  var j = 20;
          }
          console.log( i, j);    // 10 20
      })();
      
      console.log( i, j);        // Reference Error, not defined
      
  • eval不是使用全域變數物件或是呼叫者的變數物件
    使用eval會進入一個屬於eval的新執行情境
  • 在函式的情境中,變數物件被表示成一個執行中物件(Activation Object)
Δ 回到最上方

執行中物件(Activation Object, AO)

  • 執行中物件(Activation Object, AO)
    • 執行中物件(Activation Object, AO)是函式被呼叫者(Caller)呼叫變成執行狀態(Activated)時,產生的一個特殊物件
    • 執行中物件(AO)函式情境變數物件(VO)
    • AO比一般的變數物件多了變數宣告函式宣告參數arguments物件
    • AO形式參數(Formal Parameters)特殊的引數(Special Arguments)物件(具索引屬性的形式參數集合)組成
    • function foo(x, y) {
        var z = 30;
        function bar() {} 	// 函式宣告(FD)
        (function baz() {}); 	// 函式表示式(FE)
      }							 
      foo(10, 20);
      
      Figure 8. An activation object.
      • 同樣的,函式表式示(Function Expression, FE) baz,不會被含入變數物件和執行中物件
  • 關於內部函式(Inner Function)
    • 在ECMAScript中可以使用內部函式(Inner Function)
    • 內部函式可以參考到父函式的變數和全域情境的變數
    • ECMAScript將一個變數取名為情境的作用域物件(Scope Object)
    • 如果原型鏈(prototype chain)作用域物件也可形成作用域鏈(Scope Chain)
  • 補充1:Argument & Parameter
    • 引數(Argument) = 實際參數(Actual Parameter )
    • 參數(Parameter) = 形式參數(Parameter)
    • function add(n1, n2){     // n1, n2 = Parameter
          return n1 + n2; 
      }
      							
      var result = add( x, y);  // x,y = Argument
      
  • 補充2: Javascript中的arguments
    • arguments是Javascript中內建的物件,儲存傳入函式的引數資料
    • function add(){    
          for(var i = 0, result = 0; i < arguments.length; i++)
              result += arguments[i];
          return result;
      }
      
      console.log( add( 1, 2) );          // 3
      console.log( add( 1, 2, 3, 4) );    // 10
      
Δ 回到最上方

作用域鏈(Scope Chain)

  • 對情境而言,識別字(Identifiers)變數的名稱函式宣告形式參數、...等
  • 作用域鏈(Scope Chain)
    • 作用域鏈是一群物件的清單,可用情境的程式碼中使用的識別字來取得對應的值
    • 同原型鏈(Prototype Chain),如果在自身擁有的作用域(變數物件或執行中物件)中找不到變數,就尋找父變數物件,以此類推
      1. 當處理(找尋)一個識別字時,由作用域鏈的執行中物件(AO)開始搜尋
      2. 如果沒找到,則搜尋到作用域鏈頂端,重複到搜尋到或結束
    • 一般作用域鏈是所有父變數物件的清單,並在作用域鏈前端加上函式本身的變數物件(VO)/執行中物件(AO)
    • 作用域鏈也可能包含任意其它物件
      • 例如情境執行時,物件會動態的加入到作用域鏈中
      • 像with或catch語法的特殊物件。
  • 自由變數(Free Variable)
    • 當函式參考到不是區域變數的識別字(一個區域性函式或形式函式),這種變數被叫作自由變數(Free Variable)
    • 使用作用域鏈來搜尋這些自由變數
  • var x = 10;
    							
    (function foo() {
      var y = 20;
      (function bar() {
        var z = 30;
        // x, y是自由變數
        // 會在bar的作用域鏈的下一個物件被找到(bar的執行中物件後面)
        console.log(x + y + z);
      })();
    })();
    
    • 假設作用域鏈間的物件透過隱藏的__parent__屬性鏈結(Linkage),參考到鏈上的下一個物件
    • 也可以用簡單陣列來表示一個作用域鏈
    • __parent__概念,可以將上面的範例畫成下圖
      Figure 9. A scope chain.
  • 程式執行時,作用域鏈可以用敘述catch條件物件增加
    • 這些物件為簡單物件,所以有prototype和prototype chain
    • 使作用域鏈看起來像二維:
      1. 作用域鏈內的連結
      2. 每個作用域鏈之間的鏈結到原型鏈的深度
    • Object.prototype.x = 10;
       
      var w = 20;
      var y = 30;
       
      
      // 在SpiderMonkey(Mozilla用C/C++開發的Javascript引擎)的全域物件中
      // 全域情境的變數物件繼承自Object.prototype
      // 所以在prototype chain中,會出現"not defined global variable x" 
      console.log(x); // 10
       
      (function foo() {
       
        // "foo" local variables
        var w = 40;
        var x = 100;
       
        // x可以在Object.prototype中被找到
       // 因為 {z: 50}繼承自Object.prototype
        with ({z: 50}) {
          console.log(w, x, y , z); // 40, 10, 30, 50
        }
       
        // 在with物件從作用域鏈中移除後,
        // x又出現在foo情境的AO中, w也是區域變數
        console.log(x, w); // 100, 40
       
        // 瀏覽器環境中參考到全域變數w的方法
        console.log(window.w); // 20
       
      })();
      
      • 加入with物件後會產生如下結構
        Figure 10. A “with-augmented” scope chain.
    • 不是所有Javascript引擎的實作,全域物件都會繼承Object.prototype
    • 若沒有父變數物件,內部函式取得父物件的資料會變的沒有意義
      只在作用域鏈中找尋必要的變數
  • 在一個情境結束後
    • 它自身及它的狀態都會被摧毀(Destroyed)
    • 父函式可能會傳回一個內部函式,而且這個被回傳的函式可能稍後會被其它情境執行
    • 如果一些自由變數已存在時,被回傳的函式執行時會發生什麼事? 閉包的概念用來解決這個議題
Δ 回到最上方

閉包(Closure)

  • ECMAScript中,函式是第一級類別(First-Class)物件
    • 第一級類別的意思是函式可以被當作引數傳給其它函式,這種情況稱為函式引數(functional arguments, funargs)
    • 收到funargs的函式叫優先函式(Higher-Order Function)
    • 函式也可以做為其它函式的回傳值,回傳函式的函式叫做函式值函式(function valued functions)擁有函式值的函式(functions with functional value)
  • funargs、functional values衍生的2個相關議題
    • 問題是由"Funarg Problem"(或叫做函式引數問題)所衍生
    • 閉包(Closure)被發明來解決Funarg Problem
    • 接著細節深討2個子問題,並看ECMAScript如何使用函式的[[Scope]]屬性來解決這些問題
    • 上升的funarg問題(Upward funarg Problem)
      • 當函式被另一個函式回傳,函式相當於上升到外層,成為一個自由變數(Free Variable)
      • 為了能在父情境結束後存取父情境的變數
        • 內部函式在建立時會在它的[[Scope]]屬性儲存父函式的作用域鏈(Scope Chain)
      • 當函式被執行時,其情境的作用域鏈執行中物件(Activation Object)和它的[[Scope]]屬性所組成
        • Scope chain = Activation object + [[Scope]]
      • 在建立時,函式儲存其父函式的作用域鏈,將來函式被呼叫時會利用該作用域鏈來搜尋變數
      • // foo的回傳值是一個函式(bar)
        // 函式中使用到自由變數x
        function foo() {
          var x = 10;
          return function bar() {
            console.log(x);
          };
        }
         
        var returnedFunction = foo();
        										 
        // 全域變數x
        var x = 20;										
        
        // 執行回傳的函式(bar)
        returnedFunction(); 	// 顯示10, 不是20
        
        • 可以由被回傳的bar函式的[[Scope]]屬性找到變數x
        • 這類型的作用域被稱為靜態作用域
        • 動態作用域會把x當作20,而非10
        • ECMAScript不採用動態作用域
    • 下降的funarg問題(Downward funarg Problem)
      • 下降的funarg問題中,父情境可能存在,但可能無法明確辨識識別字(Identifier)
        • 函式A宣告在全域作用域下 父作用域為全域的作用域
        • 函式A被作為引數傳入函式B 父作用域為函式B的作用域
      • 存在識別字應該使用那個作用域的值的問題
        • 在函式建立時靜態儲存或是執行時動態形成(呼叫函式(Caller)的作用域)?
      • 為了避免這類問題,並形成閉包,ECMAScript決定使用靜態作用域(Static Scope)
      • var x = 10;			// 全域 x
         										
        function foo() {	// 全域函式
          console.log(x);
        }
         
        (function (funArg) {
          var x = 20;		// 區域 x
         
          // foo函式的[[Scope]]在函式建立時被靜態儲存,
          // 所以會使用全域 x
          // 而不會使用執行funArg的呼叫函式(Caller)作用域內的x										  
          funArg(); // 10, 不是20										 
        })(foo); // 將foo作為funArg往下傳送
        
  • 靜態作用域
    • 內部函式在建立時會在它的[[Scope]]屬性儲存父函式的作用域鏈(Scope Chain)
  • 靜態作用域在支援閉包的程式語言中是必需的
    • ECMAScript中只採用靜態作用域,所以可以解決Funarg Problem的2種問題
    • ECMAScript利用對函式實作[[Scope]]屬性,完整支援閉包
  • 現在可以對閉包(Closure)做一個正確的定義:
    • 「閉包由程式區塊(function)靜態儲存父作用域組成。
      透過儲存的作用域,函式可輕易的存取自由變數,因此每個函式都會在建立時儲存[[Scope]]
      所以在ECMAScript中,每個函式都是閉包。」
  • 多個函式擁有相同的父作用域
    • 例如有2個內部/全域函式
    • 這種情況下儲在[[Scope]]屬性的變數是具相同父作用域鏈(Parent Scope Chain)的函式共用的
    • 當一個閉包修改變數後,其它閉包再去取值會取到修改後的值
    • function baz() {
        var x = 1;
        return {
          foo: function foo() { return ++x; },
          bar: function bar() { return --x; }
        };
      }
       
      var closures = baz();
       
      console.log(
        closures.foo(), // 2
        closures.bar()  // 1
      );
      
      Figure 11. A shared [[Scope]].
  • 用迴圈來建立多個相同的函式
    • 如果在建立函式時,函式內使用迴圈的計數器,當每個函式都取得相同的值時,許多程式設計師常會覺得意外
    • 現在應該清楚為何會如此
      • 因為全部的函式使用同一個[[Scope]]
      • [[Scope]]的迴圈計數器的值是最後一次指派的值
    • var data = [];
       
      for (var k = 0; k < 3; k++) {
        data[k] = function () {
          alert(k);
        };
      }
       
      data[0](); // 3, 不是 0
      data[1](); // 3, 不是 1
      data[2](); // 3, 不是 2
      
    • 有多種技術可以解決這類問題。其中一種是在作用域鏈中提供一個額外的物件-例如使用一個額外的函式
    • var data = [];
      
      for (var k = 0; k < 3; k++) {
        data[k] = (function (x) {
          return function () {
            alert(x);
          };
        })(k); // 傳入 k 的值
      }
       
      // 現在結果和預其相同
      data[0]();   // 0
      data[1]();   // 1
      data[2]();   // 2
      
  • 如果想深入瞭解閉包的理論和實際應用,可以在Chapter 6. Closures中找到。要知道更多關於作用域鏈的訊息可以參考Chapter 4. Scope Chain
  • 我們將進入下個章節,考慮執行情境的最後一個屬性,即this的概念
Δ 回到最上方

This Value

  • this是和執行情境相關的一個特殊物件,因此它可以被稱作情境物件(代表執行情境被執行時的情境的物件)
  • 情境的this的值可以是任意物件
  • 常常有人錯誤的把this形容為變數物件的屬性
    • 記住:「this不是變數物件(VO)的屬性,而是執行情境裡的一個屬性」
    • 相對於變數,this不會有透過識別字取值的過程
    • 在程式中存取this時,它的值會直接由執行情境中取出,不會搜尋作用域鏈
    • this的值在進入情境時就已決定
    • 因為this不是存放在變數物件中的變數,所以在ECMAScript中不能指派新的值給this
  • 在全域情境中,this是全域物件本身(即,this此時相當於變數物件):
    • var x = 10;
      
      console.log(
        x, // 10
        this.x, // 10
        window.x // 10
      );
      
  • 在一個函式情境中,每一個函式呼叫時,this的值可能都不同
    • 此時this由呼叫函式(Caller)的呼叫敘述提供
    • foo函式(Callee)被全域情境呼叫,this的值會因不同的呼叫者(Caller)而不同s
      // foo函式的程式碼固定,但執行時this的值不同 
      function foo() {
        alert(this);
      }
       
      // 呼叫者(Caller)執行foo(Callee),
      // 並且提供this給被呼叫的函式(Callee)
       
      foo();                          // 全域物件
      foo.prototype.constructor();    // foo.prototype
       
      var bar = {
        baz: foo
      };
       
      bar.baz();                      // bar
       
      (bar.baz)();                    // bar
      (bar.baz = bar.baz)();          // 全域物件
      (bar.baz, bar.baz)();           // 全域物件
      (false || bar.baz)();           // 全域物件
       
      var otherFoo = bar.baz;
      otherFoo();                     // 全域物件
      
    • 想深入瞭解this為何在每次函式呼叫時會不同,可以參考Chapter 3,上面提及的案例都有詳細的討論
Δ 回到最上方

結論

  • 我們已簡要的瀏覽過一遍,要完整說明這些主題需要一本完整的書
  • 我們沒有提及兩個重要的主題函式和ECMAScript的演算策略,可以參考ES3的Chapter 5. Functions和Chapter 8. Evaluation Stategy
Good luck in studying ECMAScript!
Written by: Dmitry A. Soshnikov
Published on: 2010-09-02
Δ 回到最上方