文本中将由南潮首席架構師周愛民為大家介紹JavaScript語言在引擎級别的執行過程,其中包括JavaScript語言中的環境的準備,作用域及環境的差別,可執行上下文的建構及執行原理,過程中的控制和執行結果的傳回。在最後,周愛民展開文法的概念,解釋...x如何構成可執行元件。
嘉賓:周愛民,南潮首席架構師,曾擔任支付寶業務架構師,盛大網絡平台架構師。著有《大道至簡——軟體工程實踐者的思想》、《大道至易——實踐者的思想》、《Delphi源代碼分析》、《JavaScript語言精髓與程式設計實踐》等專著。
本次分享将主要圍繞以下五個方面展開:
一、環境的準備
二、執行上下文
三、過程控制
四、結果傳回
五、展開文法
1.作用域Scope
代碼當中經常出...x的一段代碼,表明可疊代對象的展開。而事實上,直接執行這段代碼并不正确。必須将其放在一段表達式内才有可能被執行。如下圖中的console.log(...x)。但是如果仔細推敲,此表達式依然無法執行,再往上追溯到第二層,可以将此表達式放到if語句中,但此時如果if語句沒有指明其作用域,則x依然無法在if語句中進行查找。此時需要為if加作用域,使其變成有塊級作用域的if語句。但是下圖中的if語句仍然無法找到x,可以再上到第三層,在function函數作用域内,x仍然沒有被找到。直到全局的範圍内才可以找到x的資訊,代碼才可以被成功執行。整個過程中會涉及到作用域的概念,下圖中藍色框的即表明作用域。
傳統的作用域的概念在JavaScript之父BrendanEich在github上的narcissns項目中有所介紹。作用域本身有兩個成員,object和parent,作用域中包含對象及屬性。Object是屬性清單,其中做變量定義以及函數定義等聲明,變量的名字可以被映射為JavaScript中的名稱清單。是以作用域主要有兩項功能,首先是查找名字,如果沒有,查找parent上一層。
2.環境Environment
作用域scope的概念在ECMAScript5(ES5)之後被替代為Environment環境,Environment取代了作用域的價值和作用。下圖中展示了環境在ES5以後的規範。首先是詞法環境規範,依然包含兩個成員,環境記錄和outer。環境記錄可以映射為作用域中的object,outer映射為作用域中的parent。此時,詞法環境規範與作用域的内容完全一緻,但不同點在于環境記錄成員是由下圖中右側的五種環境記錄規範所實作。可以發現,五種環境記錄規範中都有一個共同的方法HasBinding(N),這個方法本身隻是細化了查找名字功能。
3.屬性辨別符
ES5中較為重要的規範是屬性描述符和屬性辨別符規範。所有的環境記錄通過環境對外隻有一個有意義的Interface,即辨別符引用GetIdentifierReference。
無論哪種環境記錄都通過辨別符引用取到...x,都會将其轉化為同一種格式,如下圖。其中有base,name,strict等資訊,其中name都是一緻的。辨別符引用的作用代替了作用域中的查找名字功能。而ES5中引入辨別符引用的方式的目的是統一和規範下一步的操作,即執行上下文。
當代碼功能簡化到查找名字功能時,才開始涉及到執行。下圖中最外層是全局環境,裡面一層是函數環境,再裡面一層是詞法作用域。将代碼分為這幾種環境之後,每個環境對外public的功能就是查找名字。
二、可執行上下文Executive Context
在此基礎上,執行上下文添加了兩個成員,詞法環境和變量環境。理論上詞法環境和變量環境隻需要有一個就可以查找名字。但JavaScript中變量環境解決var聲明,詞法環境解決一般變量聲明,兩種聲明在JavaScript中不相容。
任務隊列RunJobs:任務隊列以先前先出的規則處理任務。最早放到任務隊列中的job是腳本執行job(ScriptsEvaluationJob)以及頂層子產品job (TopModuleJob),之後開始run。
執行棧Execution context stack:在此基礎上,又加了可執行元件執行棧。當執行棧為空時,自動取RunJobs中的最頂上的job,開始執行。
1.代碼層面如何run
下圖中左側的代碼塊放到JavaScript執行引擎,此時處于代碼還未正式執行,但引擎以準備好了前期工作。執行棧中有三個任務,最底層任務虛化的是初始化操作,第0個任務是newContext for job,是為任務隊列中的腳本執行job或者頂層子產品job執行的新的上下文。此時可以執行這個job,然後再建立一個scriptContext執行上下文。這裡需要注意scriptContext執行上下文和第0個任務newContext for job的執行上下文稍微不同。第0個任務的上下文是核心引擎所需要的執行上下文,而scriptContext執行上下文是JavaScript代碼可執行的上下文。此外,scriptContext執行上下文中有變量環境和詞法環境,可以通路代碼。而第0個任務newContext for job的執行上下文沒有這兩個環境。
ScriptContext執行上下文:ScriptContext執行上下文具體還可分為四種可執行的上下文,全局初始化、子產品初始化環境、執行個體化函數環境、執行個體化Eval環境等。
隻有全局上下文的準備,代碼中的console.log(...x)依然無法執行,還需準備函數環境上下文call f(),代碼才可以被執行。
三、過程的控制
函數環境在ES6之後變得非常重要,幾乎所有job都變成函數的調用。
1.生成器
下圖中從tor中擷取到對象,其中包含GeneratorContext和GeneratorState,即生成器的上下文。在生成器中,JavaScript通過函數将執行中的上下文排程交到了使用者手上。在tor.next()中即直接将生成器上下文放到執行棧中,yield可以将執行上下文從執行棧中彈出。
2.Promise
在Promise中,JavaScript通過函數将任務隊列中的任務管理交到了使用者手中。下圖中Promise需要先new,再p.then(..)。而p.then(..)的功能隻是将所接到的函數放到對象p中的Reaction清單中。此時任務隊列有兩種,resolve時的任務隊列和reject時的任務隊列。如果執行resolve,對應的任務隊列會被執行,即總會有一個任務隊列不會被執行。但此時發現下圖中的函數沒有執行語句。原因是JavaScript建立Promise時一共建立了三個對象,首先是Promise對象自己,第二個是函數resolve,第三個是函數reject,三個同時被建立。後兩個函數有同樣的内部屬性,promise内部槽,指向對象p。使用者可指定執行resolve還是reject。但如果執行棧中的全局初始化沒有完成,任務隊列仍然不會被執行。
四、結果的傳回
函數調用後會有結果的傳回,意味着ES6之後執行相關的特性都需要有值的傳回。下圖中第一行代碼會傳回true,第二行代碼傳回false。這兩行代碼代表了JavaScript兩種代碼的核心執行邏輯,執行表達式和執行語句。兩種執行邏輯傳回的結果值是不一樣的。
1.執行表達式
如下圖右側,執行表達式傳回的結果包括原始值,對象,引用規範類型。
2.執行語句
執行語句傳回的結果是完整規範類型,表示語句是否被完整執行,是否中斷,傳回值不包含引用。
執行函數是執行表達式的一種,而執行表達式隻能傳回一個值,語句不能傳回引用。可以發現...x并不滿足上述任何的傳回值。當...x放在“[ ]”中,則變成數值的展開,表示一堆element的填充,放到“()”時,則變成參數的展開。隻有在這兩種場景中才可以使用...x,[...x]/(...x)既不是語句也不是表達式,而是展開文法,是目前為數不多的可稱為文法的可執行元件。展開文法不是以表達式或語句的方式執行的,而是直接在執行位置插入代碼,即當解析資料聲明時,遇到...x,于是将其放到數組清單中。