javascript中詞法環境、領域、執行上下文以及做業詳解

網上有不少文章講到了javascript詞法環境以及執行環境,可是大多數都是說的ES5時期的詞法環境,不多是提到了ES6以及最新的ES8中有關詞法環境的介紹。相比ES5,ES6以及以後的規範對詞法環境有了不同的說明,甚至在詞法環境以外新增了領域(Realms)、做業(Jobs)這兩全新概念。這致使我在閱讀ES8的規範時遇到了很多問題,雖然最後都解決了,但爲此付出很多時間。因此我在這專門把我對詞法環境以及領域的理解寫出了。我但願經過這篇文章能對正在瞭解這一方面或對javascript有興趣的人有所幫助。好了,廢話很少說了,開始進入正題。javascript

詞法環境(Lexical Environments)

官方規範對詞法環境的說明是:詞法環境(Lexical Environments)是一種規範類型,用於根據ECMAScript代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯。詞法環境由一個環境記錄(Environment Record)和一個可能爲空的外部詞法環境(outer Lexical Environment)引用組成。一般,詞法環境與ECMAScript代碼的特定語法結構相關聯,例如FunctionDeclaration,BlockStatement或TryStatement的Catch子句,而且每次執行這樣的代碼時都會建立新的詞法環境。
環境記錄記錄了在其關聯的詞法環境做用域內建立的標識符綁定。它被稱爲詞法環境的環境記錄。環境記錄也是一種規範類型。規範類型對應於在算法中用來描述ECMAScript語言結構和ECMAScript語言類型的語義的元值。
全局環境是一個沒有外部環境的詞法環境。全局環境的外部環境引用爲null。
模塊環境是一個包含模塊頂層聲明綁定的詞法環境。模塊環境的外部環境是一個全局環境。
函數環境是一個對應於ECMAScript函數對象調用的詞法環境。
上面這些話是官方的說明,我只是稍微簡單的翻譯了一下(原諒我英語學的很差,都是谷歌的功勞)。
可能光這麼說一點都不形象,我舉個例子:html

var a,b=1;
function foo(){
   var a1,b1;
};
foo();

看上面這一簡單的代碼,js在執行這段代碼的時候作了以下操做:java

  1. 建立了一個詞法環境我把它記爲LE1(這裏的LE1實際上是一個global environment)。
  2. 肯定LE1的環境記錄(我在這不細說環境記錄,只知道它裏面包含了{a,b,foo}標識符的記錄,我會在以後詳細介紹)。
  3. 設置外部詞法環境引用,由於LE1已經在最外面了,因而外部詞法環境引用就是null,到此LE1就確立完畢了。
  4. 接着執行代碼,當執行到foo()這句話時,js調用了foo函數。此時foo函數是一個FunctionDeclaration,因而js開始執行foo函數。
  5. 建立了一個新的詞法環境記爲LE2.
  6. 設置LE2的外部詞法環境引用,很明顯LE2的外部詞法環境引用就是LE1
  7. 肯定LE2的環境記錄{a1,b1} 。
  8. 最後繼續執行foo函數,知道函數執行完畢。

注意:全部建立詞法環境以及環境記錄都是不可見的,編譯器內部實現。node

用圖簡單解釋一下LE1LE2的關係就是以下:
圖畫的真是醜算法

上面的步驟都是簡化步驟,當講解完以後的環境記錄、領域、執行上下文、做業時,我會給出一個詳細的步驟。數組

環境記錄(Environment Record)

ES8規範中主要使用兩種環境記錄值:聲明性環境記錄和對象環境記錄。環境記錄是一個抽象類,它具備三個具體的子類,分別是聲明式環境記錄,對象環境記錄和全局環境記錄。其中全局環境記錄在邏輯上是單個記錄,可是它被指定爲封裝對象環境記錄和聲明性環境記錄的組合。瀏覽器

對象環境記錄(Object Environment Record)

每一個對象環境記錄都與一個對象聯繫在一塊兒,這個對象被稱爲綁定對象(binding object)。一個對象環境記錄綁定一組字符串標識符名稱,直接對應於其綁定對象的屬性名稱。不管綁定對象本身的和繼承的屬性的[[Enumerable]]設置如何,它們都包含在集合中。因爲能夠動態地從對象中添加和刪除屬性,所以對象環境記錄綁定的一組標識符可能會由於任何添加或刪除對象屬性操做的反作用而改變。即便相應屬性的Writable的值爲false。所以因爲這種反作用而建立的任何綁定都將被視爲可變綁定。對象環境記錄不存在不可變的綁定。
with語句用到的就是對象環境記錄,咱們看一下簡單的例子:函數

var withObject={
    a:1,
    foo:function(){
        console.log(this.a);
    }
}

with(withObject){
    a=a+1;
    foo();                    //2
}

在js代碼執行到with語句的時候,性能

  1. 建立新的詞法環境。
  2. 接着建立了一個對象環境記錄即爲OEROER包含withObject這個綁定對象,OER中的字符串標識符名稱列表爲withObject中的屬性«a,foo»,在with語句中的變量操做默認在綁定對象中的屬性中優先查找。
  3. OER設置外部詞法環境引用。

注意:對象環境記錄不是指Object裏面的環境記錄。普通的Object內部不存在新的環境記錄,它的環境記錄就是定義該對象所在的環境記錄。this

聲明性環境記錄(Declarative Environment Record)

每一個聲明性環境記錄都與包含變量,常量,let,class,module,import和/或function的聲明的ECMAScript程序做用域相關聯。聲明性環境記錄綁定了包含在其做用域內聲明定義的標識符集。這句話很好理解,舉個例子以下:

import x from '***';
var a=1;
let b=1;
const c=1;
function foo(){};
class Bar{};
//這時聲明性環境記錄中就有了«x,a,b,c,foo,Bar»這樣一組標識符,固然實際存放的結構確定不是這個樣子的,還要複雜。

函數環境記錄(Function Environment Record)

函數環境記錄是一個聲明性環境記錄,它用來表示function中的頂級做用域,此外若是函數不是一個箭頭函數(ArrowFunction),則爲這個函數提供一個this綁定。若是一個函數不是一個ArrowFunction函數並引用了super,則它的函數環境記錄還包含從該函數內執行super方法調用的狀態。
函數環境記錄有下列附加的字段

字段名稱 含義
[[ThisValue]] Any 用於該函數調用的this值
[[ThisBindingStatus]] "lexical" ,"initialized" ,"uninitialized" 若是值是「lexical」,這是一個ArrowFunction,而且沒有一個本地的this值。
[[FunctionObject]] Object 一個函數對象,它的調用致使建立該環境記錄
[[HomeObject]] Object或者undefined 若是關聯的函數具備super屬性訪問權限,而且不是一個ArrowFunction,則[[HomeObject]]是該函數做爲方法綁定的對象。 [[HomeObject]]的默認值是undefined。
[[NewTarget]] Object或者undefined 若是該環境記錄是由[[Construct]]的內部方法建立的,則[[NewTarget]]就是[[Construct]]的newTarget參數的值。不然,它的值是undefined。

我簡單介紹一下這些字段,[[ThisValue]]這個字段的值就是函數中的this對象,[[ThisBindingStatus]]中"initialized" ,"uninitialized"看字面意思也知道了,主要是「lexical」這個狀態爲何是表明ArrowFunction,個人理解是ArrowFunction中是沒有一個本地的this值,因此ArrowFunction中的this引用不是指向調用該函數的對象,而是根據詞法環境進行查找,本地沒有就向外部詞法環境中查找this值,不斷向外查找,直到查到this值,因此[[ThisBindingStatus]]的值是「lexical」。看下面例子:

var a = 'global.a';
var obj1 = {
    a:'obj1.a',
    foo: function(){
     console.log(this.a);
    }
}
var obj2 = {
    a:'obj2.a',
    arrow:()=>{
     console.log(this.a);
    }
}
obj1.foo()                  //obj1.a
obj2.arrow()                //global.a不是obj2.a
obj1.foo.bind(obj2)()       //obj2.a
obj2.arrow.bind(obj1)()     //global.a  強制綁定對ArrowFunction沒有做用

對ArrowFunction中this的有趣的說法就是:我沒有this,你送我個this我也不要,我就喜歡拿別人的this用,this仍是別人的好。
[[FunctionObject]]:在上一個例子中指得就是obj1.foo、obj1.arrow。
[[HomeObject]]:只有函數有super訪問權限且不是ArrowFunction纔有值。看個MDN上的例子:

var obj1 = {
  method1() {
      console.log("method 1");
  }
}

var obj2 = {
  method2() {
      super.method1();
  }
}

Object.setPrototypeOf(obj2, obj1);
obj2.method2();                          //method 1

//在這裏obj2就是[[HomeObject]]
//注意不能這麼寫:
var obj2 = {
  foo:function method2() {
      super.method1();                 //error,function定義下不能出現super關鍵字,不然報錯。
  }
}

[[NewTarget]]:構造函數纔有[[Construct]]這個內部方法,如用new關鍵詞調用的函數就會有[[Construct]],newTarget參數咱們能夠經過new.target在函數中看到。

function newTarget(){
   console.log(new.target);
}

newTarget()             //undefined
new newTarget()         /*function newTarget(){
                              console.log(new.target);
                        }
                        new.target指代函數自己*/

全局環境記錄(Global Environment Records)

全局環境記錄用於表示在共同領域(Realms)中處理全部共享最外層做用域的ECMAScript Script元素。全局環境記錄提供了內置全局綁定,全局對象的屬性以及全部在腳本中發生的頂級聲明。
全局環境記錄有下表額外的字段。

字段名稱 含義
[[ObjectRecord]] Object Environment Record 綁定對象是一個全局對象。它包含全局內置綁定以及關聯領域的全局代碼中FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration綁定。
[[GlobalThisValue]] Object 在全局做用域內返回的this值。宿主能夠提供任何ECMAScript對象值。
[[DeclarativeRecord]] Declarative Environment Record 包含在關聯領域的全局代碼中除了FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration綁定以外的全部聲明的綁定
[[VarNames]] List of String 關聯領域的全局代碼中的FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration聲明綁定的字符串名稱。

這裏提一下FunctionDeclaration,GeneratorDeclaration,AsyncFunctionDeclaration和VariableDeclaration不在Declarative Environment Record中,而是在Object Environment Record中,這也解釋了爲何在全局代碼中用var、function聲明的變量自動的變爲全局對象的屬性而let、const、class等聲明的變量卻不會成爲全局對象的屬性。

模塊環境記錄(Module Environment Records)

模塊環境記錄是一個聲明性環境記錄,用於表示ECMAScript模塊的外部做用域。除了正常的可變和不可變綁定以外,模塊環境記錄還提供了不可變的導入綁定,這些綁定提供間接訪問另外一個環境記錄中存在的目標綁定。

領域(Realms)

在執行ECMAScript代碼以前,全部ECMAScript代碼都必須與一個領域相關聯。從概念上講,一個領域由一組內部對象,一個ECMAScript全局環境,在該全局環境做用域內加載的全部ECMAScript代碼以及其餘相關的狀態和資源組成。通俗點講領域就是老大哥,在領域下的小弟都必須等大哥把事情幹完才能作。領域被表示爲領域記錄(Realm Record),有下表的字段:

字段名稱 含義
[[Intrinsics]] 一個記錄,它的字段名是內部鍵,其值是對象 與此領域相關的代碼使用的內在值。
[[GlobalObject]] Object 這個領域的全局對象。
[[GlobalEnv]] Lexical Environment 這個領域的全局環境。
[[TemplateMap]] 一個記錄列表 { [[Strings]]: List, [[Array]]: Object}. 模板對象使用Realm Record的[[TemplateMap]]分別對每一個領域進行規範化。
[[HostDefined]] Any, 默認值是undefined. 保留字段以供須要將附加信息與Realm Record關聯的宿主環境使用。

[[Intrinsics]]:我舉幾個在[[Intrinsics]]中對你來講很熟悉的字段名%Object%(Object構造器),%ObjectPrototype%(%Object%的原型數據屬性的初始值),類似的有%Array%(Array構造器),%ArrayPrototype%、%String%、%StringPrototype%、%Function%、%FunctionPrototype%等等的內部方法,能夠說全局對象上的屬性和方法的值基本都是從[[Intrinsics]]來的(不包括宿主環境提供的屬性和方法如:console、location等)。想查看全部的內部方法請查看官方文檔內部方法列表

[[GlobalObject]]和[[GlobalEnv]]一目瞭然,在瀏覽器中[[GlobalObject]]就是值window了,node中[[GlobalObject]]就是值global。[[HostDefined]] 值宿主環境提供的附加信息。我在這重點說一下[[TemplateMap]]。

[[TemplateMap]]

[[TemplateMap]]是模板在領域中的存儲信息,每一個模板文字在領域中對應一個惟一的模板對象。具體的模板存儲方式我簡單說明一下:
在js中模板是用兩個反引號(`)進行引用;在js進行解析時模板文字被解釋爲一系列的Unicode代碼點。,具體看以下例子:

var tpObject = {name:'fqf',desc:'programmer'};
var template=`My name is${tpObject.name}. I am a ${tpObject.desc}.`;
//根據模板語法這個模板分三個部分組成:
//TemplateHead:(`My name is${),TemplateMiddle:(}. I am a ${),TemplateTail:(}.)
//tpObject.name,tpObject.desc是表達式,不存儲在模板中。
//其中若是模板文字是純字符串,則這是個NoSubstitutionTemplate。
//js是按順序解析模板文字,其中`、${、} ${、}、`被認爲是空的代碼單元序列。
//模板文字被解析成TV(模板值),TRV(模板原始值),它們之間的區別在於TRV中的轉義序列被逐字解釋,若是你的模板中不帶有(\)轉義符,你能夠認爲TV與TRV是同樣的。
//具體字符對應的編碼存儲你能夠先對字符作charCodeAt(0),而後經過toString(16)轉化爲16進制,你就知道對應的編碼單元了。

//好比字符a
('a').charCodeAt(0).toString(16);              //61,對應編碼就是0x0061

模板文字變成Unicode代碼點後,會將Unicode代碼點分段存入List,按TemplateHead,TemplateMiddleList,TemplateTail順序存入(TemplateMiddleList是多個TemplateMiddle組成的順序列表),具體表示能夠是這樣«TemplateHead,TemplateMiddle1,TemplateMiddle2,...,TemplateTail»。瞭解這個以後再來看模板信息具體是如何存入Realms的[[TemplateMap]]中的,步驟以下:

  1. 讓rawStrings成爲模板按TRV進行解析返回的結果。
  2. 讓cookedStrings成爲模板按TV進行解析返回的結果。
  3. 讓count成爲cookedStrings這個List中的元素數量。
  4. 讓template成爲ArrayCreate(count)。(ArrayCreate)是js用來建立數組的內部方法
  5. 讓rawObj成爲ArrayCreate(count)。
  6. 讓index=0。
  7. 循環,while index<count

    1. 讓prop成爲ToString(index)。
    2. cookedValue成爲cookedStrings[index]。
    3. 調用template.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: cookedValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    4. 讓rawValue成爲rawStrings[index]。
    5. 調用rawObj.[[DefineOwnProperty]](prop, PropertyDescriptor{[[Value]]: rawValue, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false})。
    6. 讓index=index+1。
  8. 凍結rawObj,相似於調用了Object.frozen(rawObj)。
  9. 調用template.[[DefineOwnProperty]]("raw", PropertyDescriptor{[[Value]]: rawObj, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false})。
  10. 凍結template。
  11. 添加Record{[[Strings]]: rawStrings, [[Array]]: template}到領域的[[TemplateMap]]中。

每一個模板都對應一個惟一且不可變的模板對象,每次獲取模板對象都是先從Realms中尋找,若是有返回模板對象,若是沒有按上面步驟添加到領域中,再返回模板對象。
因此下列tp1和tp2模板其實對應的是同一個模板對象:

var template='template';
var othertemplate='othertemplate';
var tp1=`This is a ${template}.`;
var tp2=`This is a ${othertemplate}.`;

注:我不是很清楚爲何要把模板信息存入[[TemplateMap]]中,多是考慮性能的緣由。若是有了解這方面的,但願能留言告知。

想進一步瞭解TV(模板值)和TRV(模板原始值)的不一樣請戳這裏查看具體說明。
到這裏領域的描述就告一段落了。開始進入執行上下文也稱執行環境的講解了。

執行上下文(Execution Contexts)

執行上下文是一種規範設備,經過ECMAScript編譯器來跟蹤代碼的運行時評估。在任什麼時候候,每一個代理(agent)最多隻有一個正在執行代碼的執行上下文。這被稱爲代理的運行執行上下文(running execution context)。本規範中對正在運行的執行上下文(running execution context)的全部引用都表示周圍代理的正在運行的執行上下文(running execution context)。
這看起來有點混亂,在這裏須要明白一個東西:執行上下文不是表示正在執行的上下文,你能夠把它當作一個名詞就比較好理解了。
執行上下文棧用於跟蹤執行上下文。正在運行的執行上下文始終是此堆棧的頂層元素。每當從與當前運行的執行上下文相關聯的可執行代碼轉移到與該執行上下文不相關的可執行代碼時新的執行上下文被建立。新建立的執行上下文被壓入堆棧併成爲正在運行的執行上下文。
用代碼加步驟說明:

1. var a='running execution context';
2. function foo(){
3.     console.log('new running execution context');4.
4. }
5.
6. foo();
7. console.log(a);

我把全局的執行上下文記爲ec1,
我把foo函數的執行上下文記爲ec2,
執行上下文棧記爲recList;
正在運行的執行上下文rec

  1. 首先recList是空的,rec=recList[0]。
  2. 運行全局代碼時ec1被建立,並unshift到recList中,recList=[ec1],rec=recList[0]。
  3. 當執行到第6句,進入foo函數裏時,ec2被建立並unshift到recList中,recList=[ec2,ec1],rec=recList[0]。
  4. foo函數執行完畢,recList.shift(),ec2從recList中刪除,recList=[ec1],rec=recList[0]。
  5. 到第7句執行完畢,ec1從recList中刪除,recList又變爲空了,rec=recList[0]。

在這裏咱們能夠看到執行上下文之間的轉換一般以堆棧式的後進/先出(LIFO)方式進行。
全部執行上下文都有下表的組件:

組件 含義
代碼評估狀態 任何須要去執行,暫停和恢復與此執行上下文相關的代碼評估狀態。
Function 若是這個執行上下文正在評估一個函數對象的代碼,那麼這個組件的值就是那個函數對象。若是上下文正在評估腳本或模塊的代碼,則該值爲空。
Realm 關聯代碼訪問ECMAScript資源的領域記錄。
ScriptOrModule 模塊記錄(Module Record)或腳本記錄(Script Record)相關代碼的來源。若是不存在來源的腳本或模塊,則值爲null。

正在運行的執行上下文的Realm組件的值也被稱爲當前的Realm Record。正在運行的執行上下文的Function組件的值也被稱爲活動函數對象。
ECMAScript代碼的執行上下文具備下表列出的其餘狀態組件。

組件 含義
LexicalEnvironment 標識在此執行上下文中用於解析有代碼所作的標識符引用的詞法環境。
VariableEnvironment 標識在此執行上下文中的詞法環境,它的環境記錄保存了由VariableStatements建立的綁定。

當建立執行上下文時,它的LexicalEnvironment和VariableEnvironment組件最初具備相同的值。

做業和做業隊列(Jobs and Job Queues)

做業和領域同樣都是ES6新增的東西。做業是一個抽象操做,當沒有其餘ECMAScript計算正在進行時,它將啓動ECMAScript計算。一個做業抽象操做能夠被定義爲接受任意一組做業參數。只有當沒有正在運行的執行上下文而且執行上下文堆棧爲空時,才能啓動做業的執行。一旦啓動了一個做業的執行,做業將始終執行完成。在當前正在運行的做業完成以前,不能啓動其餘做業。PendingJob是將來執行Job的請求。PendingJob是內部記錄,其字段以下表:

字段名稱 含義
[[Job]] 做業抽象操做的名稱 這是在執行此PendingJob時執行的抽象操做。
[[Arguments]] 一個List 當[[Job]]激活時要傳遞給[[Job]]的參數值的列表。
[[Realm]] 一個領域記錄 此PendingJob啓動時,最初執行上下文的領域記錄。
[[ScriptOrModule]] 一個Script Record或Module Record 此PendingJob啓動時,用於初始執行上下文的腳本或模塊。
[[HostDefined]] any,默認undefined 保留字段供須要將附加信息與 pending Job相關聯的宿主環境使用。

咱們能夠把[[Job]]當作一個函數,[[Arguments]]是這個函數的參數。
一個做業隊列是一個PendingJob記錄的FIFO隊列。每一個做業隊列都有一個名稱和由ECMAScript編譯器定義的一整套可用的做業隊列。每一個ECMAScript編譯器至少具備下表中定義的做業隊列。

名稱 目的
ScriptJobs 驗證和評估ECMAScript腳本和模塊源文本的做業。
PromiseJobs 迴應一個承諾的解決的做業

Promise的回調就是與PromiseJobs有關。

執行流程

有關javascript中詞法環境、領域、執行上下文以及做業,基本簡單的介紹了一下。那麼ECMAScript編譯器怎麼把它們之間關聯起來的呢,下面我大體寫了一個簡單的流程:
ECMAScript中有一個RunJobs ( )方法,全部東西的確立都是從這個方法出來的。

  1. 讓realm成爲CreateRealm()。CreateRealm()主要是建立了一個領域,初始化了領域中字段的值,並返回建立的領域。
  2. 讓newContext成爲一個新的執行上下文。
  3. 設置newContext的Function爲null,newContext的Realm爲realm,newContext的ScriptOrModule爲null。
  4. 把newContext放到執行上下文棧,如今newContext是一個正在運行的執行上下文。
  5. 執行SetRealmGlobalObject(realm, global, thisValue)方法,正常狀況下global爲undefined,thisValue爲undefined。

    • SetRealmGlobalObject方法執行,我在這裏默認global和thisValue爲undefined:
    1. 讓intrinsics成爲realmRec.[[Intrinsics]]。
    2. 讓globalObj等於ObjectCreate(intrinsics.[[%ObjectPrototype%]])。
    3. 讓thisValue等於globalObj。
    4. 設置realmRec.[[GlobalObject]]是globalObj。
    5. 設置newGlobalEnv爲新的詞法環境。
    6. 讓objRec成爲一個新的包含globalObj爲綁定對象的對象環境記錄。
    7. 讓dclRec成爲沒有任何綁定的新的聲明性環境記錄。
    8. 讓globalRec成爲一個新的全局環境記錄。
    9. 設置globalRec.[[ObjectRecord]]爲objRec,設置globalRec.[[GlobalThisValue]]爲 thisValue,設置globalRec.[[DeclarativeRecord]]爲dclRec,設置globalRec.[[VarNames]]是一個空的List,設置newGlobalEnv的環境記錄爲globalRec,newGlobalEnv的外部詞法環境爲null。
  6. 設置realmRec.[[GlobalEnv]]爲newGlobalEnv。
  7. 讓globalObj變爲SetDefaultGlobalBindings(realm)得返回值。SetDefaultGlobalBindings的方法主要是把realm的[[Intrinsics]]中的內部方法拷貝到全局對象中。
  8. 在globalObj上建立任何編譯器定義的全局對象屬性。
  9. 依賴編譯器方式,在零個或多個ECMAScript腳本和/或ECMAScript模塊中獲取ECMAScript源文本和任何關聯的host-defined的值。爲每個sourceText和hostDefined作以下操做:

    1. 若是sourceText是script的源代碼, 那麼執行EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »)。
    2. 若是sourceText是module的源代碼,那麼執行EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »)。
  10. 循環

    1. 掛起正在運行的執行上下文並將其從執行上下文堆棧中移除。
    2. 肯定:執行上下文堆棧如今是空的。
    3. 讓nextQueue是以編譯器定義的方式選擇的非空做業隊列。若是全部做業隊列都爲空,則結果是編譯器定義的,nextQueue裏的記錄是上面經過EnqueueJob方法放到做業隊列中的記錄。
    4. 讓nextPending成爲nextQueue前面的PendingJob記錄。從nextQueue中刪除該記錄。
    5. 讓newContext成爲一個新的執行上下文。
    6. 設置newContext的Function爲null,newContext的Realm爲nextPending.[[Realm]],newContext的ScriptOrModule爲nextPending.[[ScriptOrModule]]。
    7. 將newContext推入執行上下文堆棧; newContext如今是正在運行的執行上下文。
    8. 使用nextPending執行任何編譯器或宿主環境定義的做業初始化。
    9. 讓result成爲使用nextPending.[[Arguments]]元素做爲nextPending.[[Job]]的參數進行抽象操做的結果,這裏指運行上面EnqueueJob中的ScriptEvaluationJob或TopLevelModuleEvaluationJob方法。
    10. 若是result是忽然完成的,好比throw扔出異常, 執行HostReportErrors(« result.[[Value]] »),HostReportErrors方法就是報錯誤的,好比SyntaxError和ReferenceError等。

2017-11-27新增
忽然發現這麼一長串的步驟不易閱讀和理解,我在這作一些籠統的說明:

領域(Realm)只建立一次,領域建立後開始建立全局詞法環境(包括全局詞法環境中的聲明性環境記錄和對象環境記錄以及全局對象),SetDefaultGlobalBindings方法中global和thisValue爲undefined意味着全局環境記錄中的[[GlobalThisValue]]就是全局對象(這也表示了在瀏覽器中全局環境下this就是window對象)。

步驟9中的script中的sourceText表示用<script></script>引入的js代碼的Unicode編碼。EnqueueJob方法你能夠認爲是把腳本信息按執行順序放到隊列中。

步驟10,你能夠認爲是從隊列中拿出腳本進行執行(該循環的第9步就是執行腳本(指ScriptEvaluationJob方法),腳本的執行都是在領域和全局詞法環境建立以後的)。


我這裏說一下ScriptEvaluationJob方法的執行過程(TopLevelModuleEvaluationJob方法只在評估module時運行)正常都是運行的ScriptEvaluationJob方法。
ScriptEvaluationJob ( sourceText, hostDefined ):

  1. 肯定: sourceText是ECMAScript源文本。
  2. 讓realm成爲當前的領域記錄。
  3. 讓s成爲ParseScript(sourceText, realm, hostDefined)。
  4. 若是s是一個errors列表, 那麼執行HostReportErrors(s),返回 NormalCompletion(undefined)(一個完成記錄值,值爲undefined)。
  5. 返回ScriptEvaluation(s)。

ParseScript(sourceText, realm, hostDefined):

  1. 使用腳本解析sourceText做爲目標符號,並分析任何早期錯誤條件的解析結果。若是解析成功而且沒有發現早期錯誤,那麼讓body成爲所獲得的分析樹,不然body是一個包含一個或多個早期錯誤的列表。
  2. 若是body是錯誤列表,則返回body。
  3. 返回腳本記錄(Script Record){[[Realm]]: realm, [[Environment]]: undefined, [[ECMAScriptCode]]: body, [[HostDefined]]: hostDefined}。

早期錯誤有不少,我舉個例子:使用關鍵詞做爲標識符就是典型的早期錯誤。

ScriptEvaluation ( scriptRecord )大體流程:

  1. 讓globalEnv成爲scriptRecord.[[Realm]].[[GlobalEnv]]。
  2. 讓scriptCxt成爲一個新的ECMAScript代碼執行上下文。
  3. 設置scriptCxt的Function爲null, scriptCxt的Realm爲scriptRecord.[[Realm]],設置scriptCxt的ScriptOrModule爲scriptRecord。
  4. 設置VariableEnvironment和LexicalEnvironment爲scriptCxt的globalEnv
  5. 掛起當前正在運行的執行上下文。
  6. 把scriptCxt放到執行上下文棧中,scriptCxt是一個正在運行的執行上下文。
  7. 讓scriptBody成爲scriptRecord.[[ECMAScriptCode]]。
  8. 讓result成爲運行GlobalDeclarationInstantiation(scriptBody, globalEnv)返回的結果。
  9. 若是result.[[Type]]是normal,那麼設置result是執行scriptBody的結果.
  10. 若是result.[[Type]]是normal且result.[[Value]]是empty, 那麼設置result爲NormalCompletion(undefined).
  11. 掛起scriptCxt並將其從執行上下文堆棧中刪除。
  12. 將當前位於執行上下文堆棧頂部的上下文恢復爲正在運行的執行上下文。
  13. 返回Completion(result),一個記錄值。

GlobalDeclarationInstantiation()方法是對全局環境中的標識符定義進行實例化。好比var、function、let、const、class聲明的標識符。該方法執行成功返回的result.[[Type]]爲normal。注意這時候的咱們能看到的js代碼尚未執行,真正執行咱們的代碼的是步驟9。這也是爲何咱們用var和function聲明的標識符會出現變量提高(Hoisting)現象。let、const、class聲明也在步驟9以前,之因此沒有變量提高是由於let、const、class聲明的標識符只進行實例化而沒有初始化,在下一篇文章中我會重點介紹它們之間的不一樣之處(因此我認爲那些說var和function聲明存在變量提高,而let、const、class聲明的變量不提高的說法是不對的)。


2017-11-27新增
ScriptEvaluation你能夠簡單的認爲它作了兩件:1.對標識符實例化以及初始化,2.執行javascript腳本。

GlobalDeclarationInstantiation方法只對當前腳本的標識符定義進行實例化,不能跨腳本。好比script1在script2以前引用,那麼script2中的聲明的變量只有經過GlobalDeclarationInstantiation實例化後才能在script1中引用,這也表示var和function聲明的標識符不能跨腳本進行變量提高。


結束語

到這裏本篇文章也快結束了,本文章全部的說法都是以最新的ECMAScript的語言規範(ES8)爲基礎。但願這篇文章能夠幫助你們更加深刻的瞭解javascript,若是本文有不當之處請指出。還有我不得不吐槽一下ECMAScript的語言規範寫得真是太不友好了,看得我心好累啊(說到底仍是本身當初在英語課上睡覺的鍋)。最後若是你想看ECMAScript的語言規範,那麼第5章和第6章必定要看!必定要看!這是一個過來人的忠告。

相關文章
相關標籤/搜索