網上有不少文章講到了javascript詞法環境以及執行環境,可是大多數都是說的ES5時期的詞法環境,不多是提到了ES6以及最新的ES8中有關詞法環境的介紹。相比ES5,ES6以及以後的規範對詞法環境有了不同的說明,甚至在詞法環境以外新增了領域(Realms)、做業(Jobs)這兩全新概念。這致使我在閱讀ES8的規範時遇到了很多問題,雖然最後都解決了,但爲此付出很多時間。因此我在這專門把我對詞法環境以及領域的理解寫出了。我但願經過這篇文章能對正在瞭解這一方面或對javascript有興趣的人有所幫助。好了,廢話很少說了,開始進入正題。javascript
官方規範對詞法環境的說明是:詞法環境(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
注意:全部建立詞法環境以及環境記錄都是不可見的,編譯器內部實現。node
用圖簡單解釋一下LE1和LE2的關係就是以下:
算法
上面的步驟都是簡化步驟,當講解完以後的環境記錄、領域、執行上下文、做業時,我會給出一個詳細的步驟。數組
ES8規範中主要使用兩種環境記錄值:聲明性環境記錄和對象環境記錄。環境記錄是一個抽象類,它具備三個具體的子類,分別是聲明式環境記錄,對象環境記錄和全局環境記錄。其中全局環境記錄在邏輯上是單個記錄,可是它被指定爲封裝對象環境記錄和聲明性環境記錄的組合。瀏覽器
每一個對象環境記錄都與一個對象聯繫在一塊兒,這個對象被稱爲綁定對象(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語句的時候,性能
注意:對象環境記錄不是指Object裏面的環境記錄。普通的Object內部不存在新的環境記錄,它的環境記錄就是定義該對象所在的環境記錄。this
每一個聲明性環境記錄都與包含變量,常量,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中的頂級做用域,此外若是函數不是一個箭頭函數(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指代函數自己*/
全局環境記錄用於表示在共同領域(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等聲明的變量卻不會成爲全局對象的屬性。
模塊環境記錄是一個聲明性環境記錄,用於表示ECMAScript模塊的外部做用域。除了正常的可變和不可變綁定以外,模塊環境記錄還提供了不可變的導入綁定,這些綁定提供間接訪問另外一個環境記錄中存在的目標綁定。
在執行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]]是模板在領域中的存儲信息,每一個模板文字在領域中對應一個惟一的模板對象。具體的模板存儲方式我簡單說明一下:
在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]]中的,步驟以下:
循環,while index<count
每一個模板都對應一個惟一且不可變的模板對象,每次獲取模板對象都是先從Realms中尋找,若是有返回模板對象,若是沒有按上面步驟添加到領域中,再返回模板對象。
因此下列tp1和tp2模板其實對應的是同一個模板對象:
var template='template'; var othertemplate='othertemplate'; var tp1=`This is a ${template}.`; var tp2=`This is a ${othertemplate}.`;
注:我不是很清楚爲何要把模板信息存入[[TemplateMap]]中,多是考慮性能的緣由。若是有了解這方面的,但願能留言告知。
想進一步瞭解TV(模板值)和TRV(模板原始值)的不一樣請戳這裏查看具體說明。
到這裏領域的描述就告一段落了。開始進入執行上下文也稱執行環境的講解了。
執行上下文是一種規範設備,經過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
在這裏咱們能夠看到執行上下文之間的轉換一般以堆棧式的後進/先出(LIFO)方式進行。
全部執行上下文都有下表的組件:
組件 | 含義 |
---|---|
代碼評估狀態 | 任何須要去執行,暫停和恢復與此執行上下文相關的代碼評估狀態。 |
Function | 若是這個執行上下文正在評估一個函數對象的代碼,那麼這個組件的值就是那個函數對象。若是上下文正在評估腳本或模塊的代碼,則該值爲空。 |
Realm | 關聯代碼訪問ECMAScript資源的領域記錄。 |
ScriptOrModule | 模塊記錄(Module Record)或腳本記錄(Script Record)相關代碼的來源。若是不存在來源的腳本或模塊,則值爲null。 |
正在運行的執行上下文的Realm組件的值也被稱爲當前的Realm Record。正在運行的執行上下文的Function組件的值也被稱爲活動函數對象。
ECMAScript代碼的執行上下文具備下表列出的其餘狀態組件。
組件 | 含義 |
---|---|
LexicalEnvironment | 標識在此執行上下文中用於解析有代碼所作的標識符引用的詞法環境。 |
VariableEnvironment | 標識在此執行上下文中的詞法環境,它的環境記錄保存了由VariableStatements建立的綁定。 |
當建立執行上下文時,它的LexicalEnvironment和VariableEnvironment組件最初具備相同的值。
做業和領域同樣都是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 ( )方法,全部東西的確立都是從這個方法出來的。
執行SetRealmGlobalObject(realm, global, thisValue)方法,正常狀況下global爲undefined,thisValue爲undefined。
依賴編譯器方式,在零個或多個ECMAScript腳本和/或ECMAScript模塊中獲取ECMAScript源文本和任何關聯的host-defined的值。爲每個sourceText和hostDefined作以下操做:
循環
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 ):
ParseScript(sourceText, realm, hostDefined):
早期錯誤有不少,我舉個例子:使用關鍵詞做爲標識符就是典型的早期錯誤。
ScriptEvaluation ( scriptRecord )大體流程:
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章必定要看!必定要看!這是一個過來人的忠告。