由 ECMA 規範解讀 Javascript 可執行上下文概念

前言

其實規範這東西不是給人看的,它更多的是給語言實現者提供參考。可是當碰到問題找不到答案時,規範每每能提供想要的答案 。偶爾讀一下可以帶來很大的啓發和思考,若是隻讀一章 Javascript 規範,大神們以爲非第10章莫屬。javascript

咱們來試試看,此次選用的是 ECMA2.2的 5.1 版,整個規範才200頁, 而第10章共10頁,能夠感覺到 Javascript 的精簡,目前的版本加了太多 ES6 的東西,讓人望而生畏。css

資料地址:http://www.ecma-international...html

任務

閱讀 ECMA262 5.1 第10章 Executable Code and Execution Contexts (可執行代碼與執行上下文)
你能針對這章內容提出問題嗎? 即知道答案找出問題。
你能使用圖來更形象地表達文章內容嗎?html5

開始咱們的探險之旅

原汁原味 ECMAScript 5.1 英文版
平易近人 ECMAScript 5.1 中文版java

可執行代碼類型

v8JavaScript 引擎都是按照 ecma-262 的規範來實現的,JavaScript 引擎在解釋 JavaScript 代碼時,將可執行代碼分爲了三種。分別是:git

  • 全局代碼github

    代碼加載時首先進入的環境。
    例如加載外部的 JavaScript 文件或者本地 <script></script> 標籤內的代碼。
    但不包括任何 function 體內的代碼。
  • 函數代碼web

    是指做爲 function 被解析的源代碼。
    不包括做爲其嵌套函數的 function 被解析的源代碼。
    由於 JavaScript 函數中還能夠嵌套函數,所以這也是三種可執行代碼中最複雜的一種。
  • eval代碼segmentfault

    指的是傳遞給 eval 內置函數的代碼。

注:不瞭解 eval(string) 的小夥伴,請參考 eval() - JavaScript | MDN瀏覽器

JavaScript 引擎開始執行(進入)一段可執行代碼以後,會生成一個執行環境(Execution Context),或執行上下文。引擎用執行環境來維護執行當前代碼所須要的變量聲明、this指向等。

圖片描述

詞法環境 (Lexical Environments)

詞法環境 是執行環境的三個組成的狀態之一。

官方解釋:詞法環境是用來定義特定變量和函數標識符的。一個詞法環境由一個環境記錄項和可能爲空的外部詞法環境引用構成。

一般詞法環境會與 ECMAScript 代碼諸如 函數聲明(FunctionDeclaration)WithStatement 或者 TryStatementCatch 塊這樣的特定句法結構相聯繫,且相似代碼每次執行都會有一個新的詞法環境被建立出來。

外部詞法環境引用 用於表示詞法環境的邏輯嵌套關係模型。(內部)詞法環境的外部引用是邏輯上包含內部詞法環境的詞法環境。外部詞法環境天然也可能有多個內部詞法環境。

例如,若是一個 FunctionDeclaration 包含兩個嵌套的 FunctionDeclaration,那麼每一個內嵌函數的詞法環境都是外部函數本次執行所產生的詞法環境。

環境記錄項 又能夠分爲兩種聲明式環境記錄項對象式環境記錄項

聲明式環境記錄項 用於標識標識符和函數聲明變量聲明catch 語句等語法元素的綁定。對象式環境記錄項 主要用於定義那些將標識符與具體對象的屬性綁定的語法元素。

咬文嚼字,很差理解?

通俗點講: 詞法環境就是 JavaScript 引擎在執行代碼過程當中用來標識函數聲明、變量聲明這一類的。咱們每次聲明一個函數,或者使用 withcatch語句的時候,就會有新的詞法環境被建立出來。全局詞法環境的外部詞法環境就是空的,由於他已是最外層的詞法環境了。

咱們用個例子來講明詞法環境:

var x = 10;
function foo(y){
    var z = 30;
    function bar(q){
        return x+y+z+q;
    }
    return bar;
}
var bar = foo(20);
bar(40);

圖片描述

詞法環境的運算

給出一個標識符字符串,首先在當前的詞法環境內尋找,若是存在,返回引用的標識符字符串,若是不存在,再在當前詞法環境的外部詞法環境尋找。

咦,怎麼感受和做用域鏈的概念很類似?他們有什麼關係嗎?

  • 執行環境 當執行流進入一個函數時,函數的環境會被推入一個環境棧中。當函數執行以後,環境棧將其彈出,把控制權返回給以前的執行環境。
  • 做用域鏈 當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈。其用途是保證對執行環境有權訪問的全部變量和函數的有序訪問。一個包含環境的變量對象到另外一個包含環境的變量對象,最後到全局執行環境的變量對象。

函數只要被建立,就會有本身的「地盤」,有本身的做用域。可是隻有函數被執行的時候,纔會有本身的執行環境。函數執行完畢的時候,執行環境就會退出。並且一個做用域下可能存在多個執行環境,好比閉包。

小總結

一、詞法環境分爲了兩部分:環境記錄項和外部詞法環境。
二、環境記錄項根據綁定的 ECMA 腳本元素的不一樣也分爲了兩部分。
三、函數聲明或者使用 withcatch語句時,就會有新的詞法環境被建立出來。

執行環境(Execution Contexts)

若是咱們的 JavaScript 程序有各類函數,函數之間還有嵌套的狀況,那 JavaScript 引擎怎麼解釋各類聲明和執行上下文哪?

當控制器轉入 ECMA 腳本的可執行代碼時,上文已經說了有三種可執行代碼,無論進入哪種控制器都會進入一個執行環境。多個執行環境在邏輯上造成一個棧結構。棧結構最頂層的執行環境稱爲當前運行的執行環境,最底層是全局執行環境。

用一張圖解釋

clipboard.png

由於 JS 引擎被實現爲單線程,也就是同一時間只能發生一件事情,其餘的行爲就會依次排隊。

你能夠有任意多個函數執行環境,每次調用函數建立一個新的執行環境,會建立一個私有做用域,函數內部聲明的任何變量都不能在當前函數做用域外部直接訪問。函數能訪問當前執行環境外面的變量聲明,但在外部執行環境不能訪問內部的變量/函數聲明。

小總結

關於執行棧(調用棧)

單線程。
同步執行。
一個全局上下文。
無限制函數上下文。
每次函數被調用建立新的執行上下文,包括調用本身。
return 或者拋出異常退出一個執行環境。

咱們用一個具體的函數理解:

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(2);

// 輸出:

// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2

clipboard.png

代碼的執行流程進入內部函數,建立一個新的執行上下文並把它壓入執行棧的頂部。瀏覽器總會執行位於棧頂的執行上下文,一旦當前上下文函數執行結束,它將被從棧頂彈出,並將上下文控制權交給當前的棧。 這樣,堆棧中的上下文就會被依次執行而且彈出堆棧,直到回到全局的上下文。

執行環境包含全部用於追蹤與其相關的代碼的執行進度的狀態。精確地說,每一個執行環境包含以下表列出的組件。

執行環境的三個狀態

組件 做用目的
詞法環境 指定一個詞法環境對象,用於解析該執行環境內的代碼建立的標識符引用。
變量環境 指定一個詞法環境對象,其環境數據用於保存由該執行環境內的代碼經過 變量表達式 和 函數表達式 建立的綁定。
this 綁定 指定該執行環境內的 ECMA 腳本代碼中 this 關鍵字所關聯的值。

當建立一個執行環境時,其詞法環境組件和變量環境組件最初是同一個值。在該執行環境相關聯的代碼的執行過程當中,變量環境組件永遠不變,而詞法環境組件有可能改變。變量環境的不變和詞法環境的可能改變都是指引用的改變。

圖片描述

創建執行環境

解釋執行全局代碼或使用 eval 函數輸入的代碼會建立並進入一個新的執行環境。每次調用 ECMA 腳本代碼定義的函數也會創建並進入一個新的執行環境,即使函數是自身遞歸調用的。

每一次 return 都會退出一個執行環境。拋出異常也可退出一個或多個執行環境。

當控制流進入一個執行環境時,會設置該執行環境的 this 綁定組件,定義變量環境和初始詞法環境,並執行聲明式綁定初始化過程。以上這些步驟的嚴格執行方式由進入的代碼的類型決定。

進入全局代碼

執行如下步驟:

一、將變量環境設置爲 全局環境 。 
二、將詞法環境設置爲 全局環境 。
三、將 this 綁定設置爲 全局對象 。
四、使用全局代碼執行聲明式綁定初始化化步驟。

進入函數代碼

當控制流根據一個函數對象 F、調用者提供的 thisArg 以及調用者提供的 argumentList,進入函數代碼的執行環境時,執行如下步驟

若是函數代碼是嚴格模式下的代碼,設 this 綁定 爲 thisArg。
不然若是 thisArg 是 null 或 undefined,則設 this 綁定 爲全局對象。
不然若是 Type(thisArg) 的結果不爲 Object,則設 this 綁定 爲 ToObject(thisArg)。
不然設 this 綁定 爲 thisArg。
以 F 的 [[Scope]] 內部屬性爲參數調用 NewDeclarativeEnvironment(新建聲明式詞法環境),並令 localEnv 爲調用的結果。
設 詞法環境組件 爲 localEnv。
設 變量環境組件 爲 localEnv。
令 code 爲 F 的 [[Code]] 內部屬性的值。
使用函數代碼 code 和 argumentList 執行聲明式綁定初始化化步驟。

咱們用僞代碼表示一下:

if(是 嚴格模式) {
    this = thisArg
} else if(thisArg === null || thisArg === undefined) {
    this = window
} else if(typeof thisArg != 'object') {
    this = Object(thisArg)
} else {
    this = thisArg
}

哎,這裏的 thisArg 指的是什麼?上文說了 thisArg 來自於函數的調用者。

這裏表明函數的 applycallbind 等設置 this 綁定的參數:

經過 call 或者 apply 調用函數時,thisArg 的值比較明顯,爲傳入的第一個參數。

Function.prototype.apply (thisArg, argArray)

Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] )

固然 thisArg 還有其餘的可能,具體的能夠參考這篇文章 Javascript this 解析

對上面的僞代碼作一下解釋:

嚴格模式: 也就是說,在嚴格模式下,this 只能爲 thisArg,而當 thisArgundefined 時,this 就是 undefined ,而不是 window

非嚴格模式: 若是 thisArgnull (如 fun.call(null)) 或 undefined (直接調用函數),則 this 爲全局對象,瀏覽器裏就是 window

不然,若是 傳入了 thisArg, 但不是個對象,則把它轉爲對象,並賦給 this,好比,當 fun.call('hhh') 時,打印 fun 內的 this

String {0: "h", 1: "h", 2: "h", length: 3, [[PrimitiveValue]]: "hhh"}

不然 ,也就是僅剩的一種狀況,顯式的傳入了一個對象做爲 thisArg 參數的狀況下,設 this 綁定爲 thisArg

聲明式綁定初始化

每一個執行環境都有一個關聯的 變量環境。當在一個執行環境下評估一段 ECMA 腳本時,變量和函數定義會以綁定的形式添加到這個 變量環境 的環境記錄中。對於函數代碼,參數也一樣會以綁定的形式添加到這個 變量環境 的環境記錄中。

總結

ECMAScript 代碼的執行由運行環境來完成。不一樣的運行環境可能採起不一樣的執行方式,但基本的流程是相同的。如瀏覽器在解析 HTML 頁面中遇到 <script> 元素時,會下載對應的代碼來運行,或直接執行內嵌的代碼。代碼的基本執行方式是從上到下,順序執行。在調用函數以後,代碼的執行會進入一個執行上下文之中。因爲在一個函數的執行過程當中會調用其餘的函數,執行過程當中的活動執行上下文會造成一個堆棧結構。在棧頂的是當前正在執行的代碼。當函數返回時,會退出當前的執行上下文,而回到以前的執行上下文中。若是代碼執行中出現異常,則可能從多個執行上下文中退出。

在代碼執行過程當中很重要的一步是標識符的解析。好比當執行過程當中遇到語句 alert(val) 時,首先要作的是解析標識符 val 的值。ECMAScript 不一樣於 JavaC/C++ 等語言,在進行標識符解析時須要利用詞法環境並與函數調用方式相關。具體來講,標識符解析由當前代碼所對應的執行上下文來完成。爲了描述標識符的解析過程,ECMAScript 規範中使用了詞法環境的概念來進行描述。一個詞法環境描述了標識符與變量或函數之間的對應關係。一個詞法環境由兩個部分組成:一部分是記錄標識符與變量之間的綁定關係的環境記錄,另外一部分是包圍當前詞法環境的外部詞法環境。環境記錄能夠當作是一個標識符與變量或函數之間的映射表。不一樣詞法環境之間能夠互相嵌套,而內部詞法環境會持有一個包圍它的外部詞法環境的引用。在進行標識符解析時,若是當前詞法環境中找不到標識符所對應的變量或函數,則使用外部詞法環境來嘗試解析。遞歸查找下去,直到解析成功或外部詞法環境爲 null

具體來講,根據標識符關聯方式的不一樣,環境記錄能夠進一步分紅兩類。兩種類型分別對應不一樣的 ECMAScript 中不一樣的語法結構。當使用這些語法結構時,會對環境記錄中的內容產生影響,進而影響標識符的解析過程。第一類環境記錄是聲明式環境記錄。顧名思義,聲明式環境記錄用來綁定 ECMAScript 代碼中的變量聲明。當使用 var 聲明變量或使用相似 function func(){} 的形式聲明函數時,對應的變量或函數會被綁定到相應的環境記錄中。另外一類環境記錄是對象環境記錄。對象環境記錄並不綁定具體的變量或函數,而是綁定另一個對象中的屬性。對象環境變量主要用來描述 ECMAScriptwith 操做符的行爲。

每一個執行上下文會對應兩個不一樣的詞法環境。一個是用來進行標識符解析的詞法環境,可能隨着代碼的執行而發生變化;另一個是包含執行上下文對應的做用域中的變量或函數聲明的詞法環境。

提問

讀完這篇文章,問問本身,可以回答下面的問題嗎?

一、ECMAScript 中可執行代碼有幾種?
二、什麼狀況下會建立一個執行環境?
三、什麼狀況下會退出一個執行環境?
四、做用域鏈和執行環境的關係?
五、執行環境的存在是爲了解決什麼?
六、詞法環境和變量環境的異同?
七、this 的綁定的幾種狀況?

參考

ES5/可執行代碼與執行環境
深刻理解JavaScript系列(11):執行上下文(Execution Contexts)
瞭解JavaScript的執行上下文
深刻理解JavaScript執行上下文、函數堆棧、提高的概念
關於js做用域那些事
從 ECMAScript 規範來看 JS 的 this 綁定規則
深刻探討 ECMAScript 規範

推薦閱讀

JavaScript欲速則不達—經過解析過程瞭解JavaScript

相關文章
相關標籤/搜索