Javascript実行フローの詳細原理解析


Javascriptは定義から実行に至るまで、JSエンジンは実現層に多くの初期化作業を行っているので、JSエンジンの動作メカニズムを学ぶ前に、環境スタック、グローバルオブジェクト、実行環境、変数オブジェクト、活動対象、作用ドメイン、および作用ドメインチェーンなどをいくつかの関連概念を導入する必要があります。この文章の目的は孤立してあなたのためにすべての概念を説明するのではなく、簡単なDEMOを通じて分析を展開し、大域的にJSエンジンの定義から実行までの詳細、およびこれらの概念がその中で演じた役割を説明することです。

var x = 1; //         x
function A(y){
  var x = 2; //         x
  function B(z){ //         B
    console.log(x+y+z);
  }
  return B; //    B   
}
var C = A(1); //  A,  B
C(1); //    B,   4
次にグローバル初期化、実行関数A、実行関数Bの3段階に分けて、JSエンジンの動作メカニズムを分析します。
1.グローバル初期化
JSエンジンは実行可能なコードを入力する時、以下の3つの初期化作業を完了する必要があります。
まず、グローバルオブジェクト(Global Object)を作成します。このオブジェクトは全体的に一つしか存在しません。属性はどこにでもアクセスできます。その存在はアプリケーションのライフサイクル全体に伴っています。グローバルオブジェクトは、作成時に、Math、String、Date、documentなどの一般的なJSオブジェクトをその属性とします。この全体の対象は名前で直接アクセスできないので、もう一つの属性のwindowがあります。そしてwindowを自分に向けて、windowを通じてこの全体の対象を訪問することができます。グローバルオブジェクトを疑似コードでシミュレートする概略構成は以下の通りである。

//        
var globalObject = {
  Math:{},
  String:{},
  Date:{},
  document:{}, //DOM  
  ...
  window:this // window       
}
その後、JSエンジンは実行環境スタック(Execution Contect Stock)を構築する必要があり、同時に、グローバル実行環境(Execution Conteet)ECを作成し、このグローバル実行環境ECを実行環境スタックに押し込む必要があります。環境スタックを実行する役割は、プログラムが正しい順序で実行されることを保証するためである。javascriptでは、それぞれの関数に実行環境があり、関数を実行すると、その関数の実行環境が実行環境スタックのトップに押し入れられ、実行権が得られます。この関数が実行されると、その実行環境はまたこのスタックの上部から削除され、実行権を元に環境を返します。環境スタックとECの関係を実行するために擬似コードを使ってシミュレーションした。

var ECStack = []; //         ,     
var EC = {};  //        ,
//ECMA-262      EC           ,                 
ECStack.push(EC); //    ,      
ECStack.pop(EC); //     ,      
最後に、JSエンジンはECに関連するグローバル変数オブジェクトVOを作成し、VOをグローバルオブジェクトに向ける。VOにはグローバルオブジェクトの固有属性だけでなく、グローバルに定義された変数xと関数Aも含まれている。同時に、関数Aを定義する際に、Aの内部属性scopeを追加し、scopeをVOに向けた。各関数は定義する時、関連するscope属性を作成します。scopeはいつも関数を定義する時にある環境を指します。この時のECStock構造は以下の通りです。

ECStack = [  //     
  EC(G) = {  //      
    VO(G):{ //        
      ... //           
      x = 1; //    x
      A = function(){...}; //    A
      A[[scope]] = this; //  A scope,    VO  
    }
  }
];
2.実行関数A
A(1)に入ると、JSエンジンは以下の作業を完了する必要があります。
まず、JSエンジンは、関数Aの実行環境ECを作成し、ECは、実行環境スタックの上部に押し込み、実行権を取得する。このとき実行環境スタックには、グローバル実行環境と関数A実行環境の二つがあり、Aの実行環境はスタックの一番上にあり、グローバル実行環境はスタックの底にある。
その後、関数Aのスコープチェーンを作成し、Javascriptにおいて、各実行環境は、識別子解析に用いられ、実行環境が作成されると、そのスコープは現在の実行関数のscopeに含まれるオブジェクトに初期化される。
次に、JSエンジンは現在の関数の活動対象AOを作成します。ここでの活動対象は変数の対象となる役割を演じています。関数の呼び方が違っています。また、局所変数と内部関数の定義を行い、AOは作用するドメインチェーンの先端に押し込まれます。関数Bを定義する際には、JSエンジンも同様にBにscope属性を追加し、scopeを定義関数Bを指した場合の環境を定義し、関数Bを定義する環境はAの活動対象AOであり、AOはチェーンテーブルの先端に位置し、チェーンテーブルが最初の尾につながる特徴があるため、関数BのscopeはAの全作用領域チェーンを指す。この時のECStock構造を見てみます。

ECStack = [  //     
  EC(A) = {  //A     
    [scope]:VO(G), //VO       
    AO(A) : { //    A     
      y:1,
      x:2, //      x
      B:function(){...}, //    B
      B[[scope]] = this; //this  AO  , AO  scopeChain   ,  B[[scope]]        
      arguments:[],//           arguments  AO  arguments
      this:window //    this     window  
    },
    scopeChain:<AO(A),A[[scope]]> //      A[[scope]],    AO          ,  A     :AO(A)->VO(G)
  },
  EC(G) = {  //      
    VO(G):{ //        
      ... //           
      x = 1; //    x
      A = function(){...}; //    A
      A[[scope]] = this; //  A scope,A[[scope]] == VO(G)
    }
  }
];
3.実行関数B
関数Aが実行された後、Bの参照を返し、変数Cに値を割り当てました。実行C(1)はB(1)に相当します。JSエンジンは以下の作業を完了する必要があります。
まず、上記と同様に、関数Bの実行環境ECを作成し、その後、ECが実行環境スタックのトップに押し入れて実行権を取得する。このとき実行環境スタックには、グローバル実行環境と関数Bの実行環境という二つの実行環境があり、Bの実行環境はスタックトップにあり、グローバル実行環境はスタックの底にある(注:関数Aが戻ると、Aの実行環境はスタックから削除され、グローバル実行環境のみが残されます。その後、関数Bの作用領域チェーンを作成し、関数Bのscopeに含まれるオブジェクト、すなわちAの作用領域チェーンを含むものに初期化します。最後に、関数BのアクティブオブジェクトAOを作成し、Bのイメージパラメータz、アーグメンツオブジェクト、およびthisオブジェクトをAOの属性とします。この時、ECStockはこうなります。

ECStack = [  //     
  EC(B) = {  //  B     ,          
    [scope]:AO(A), //    A     ,AO(A)->VO(G)
    var AO(B) = { //    B     
      z:1,
      arguments:[],
      this:window
    }
    scopeChain:<AO(B),B[[scope]]> //      B[[scope]],  AO(B)      ,  B     :AO(B)->AO(A)-VO(G)
  },
  EC(A), //A             ,
  EC(G) = {  //      
    VO:{ //        
      ... //           
      x = 1; //    x
      A = function(){...}; //    A
      A[[scope]] = this; //  A scope,A[[scope]] == VO(G)
    }
  }
];
関数Bが「x+y+z」を実行する場合、x、y、zの3つの識別子を1つずつ解析し、解析過程は変数の検索規則に従います。まず自分の活動対象の中にこの属性が存在するかどうかを調べます。もし存在すれば、検索を停止して戻ります。存在しない場合は、その作用するドメインチェーンに沿ってトップから順に検索し続けます。見つけられるまで、ドメインチェーン全体にその変数が見つからない場合は、「undefined」に戻ります。上記の分析から、関数Bの作用するドメインチェーンがこのように見える。
AO(B)->AO(A)->VO(G)
したがって、変数xは、VO(G)のxを検索することなく、AO(A)の中に発見されます。変数zは自身のAO(B)の中にあります。だから実行結果:2+1+1=4.
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。