壹 ❀ 引html
咱們都知道,JS代碼的執行順序老是與代碼前後順序有所差別,當先拋開異步問題你會發現就算是同步代碼,它的執行也與你的預期不一致,好比:git
function f1() { console.log('聽風是風'); }; f1(); //echo function f1() { console.log('echo'); }; f1(); //echo
按照代碼書寫順序,應該先輸出 聽風是風,再輸出 echo纔對,很遺憾,兩次輸出均爲 echo;若是咱們將上述代碼中的函數聲明改成函數表達式,結果又不太同樣:github
var f1 = function () { console.log('聽風是風'); }; f1(); //聽風是風 var f1 = function() { console.log('echo'); }; f1(); //echo
這說明代碼在執行前必定發生了某些微妙的變化,JS引擎究竟作了什麼呢?這就不得不提JS執行上下文的了。數組
貳 ❀ JS執行上下文瀏覽器
JS代碼在執行前,JS引擎總要作一番準備工做,這份工做其實就是建立對應的執行上下文;異步
執行上下文有且只有三類,全局執行上下文,函數上下文,與eval上下文;因爲eval通常不會使用,這裏不作討論。函數
1.全局執行上下文post
全局執行上下文只有一個,在客戶端中通常由瀏覽器建立,也就是咱們熟知的window對象,咱們能經過this直接訪問到它。this
全局對象window上預約義了大量的方法和屬性,咱們在全局環境的任意處都能直接訪問這些屬性方法,同時window對象仍是var聲明的全局變量的載體。咱們經過var建立的全局對象,均可以經過window直接訪問。spa
2.函數執行上下文
函數執行上下文可存在無數個,每當一個函數被調用時都會建立一個函數上下文;須要注意的是,同一個函數被屢次調用,都會建立一個新的上下文。
說到這你是否會想,上下文種類不一樣,並且建立的數量還這麼多,它們之間的關係是怎麼樣的,又是誰來管理這些上下文呢,這就不得不說說執行上下文棧了。
叄 ❀ 執行上下文棧(執行棧)
執行上下文棧(下文簡稱執行棧)也叫調用棧,執行棧用於存儲代碼執行期間建立的全部上下文,具備LIFO(Last In First Out先進後出)的特性。
JS代碼首次運行,都會先建立一個全局執行上下文並壓入到執行棧中,以後每當有函數被調用,都會建立一個新的函數執行上下文並壓入棧內;因爲執行棧LIFO的特性,因此能夠理解爲,JS代碼執行完畢前在執行棧底部永遠有個全局執行上下文。
function f1() { f2(); console.log(1); }; function f2() { f3(); console.log(2); }; function f3() { console.log(3); }; f1();//3 2 1
咱們經過執行棧與上下文的關係來解釋上述代碼的執行過程,爲了方便理解,咱們假象執行棧是一個數組,在代碼執行初期必定會建立全局執行上下文並壓入棧,所以過程大體以下:
//代碼執行前建立全局執行上下文 ECStack = [globalContext]; // f1調用 ECStack.push('f1 functionContext'); // f1又調用了f2,f2執行完畢以前沒法console 1 ECStack.push('f2 functionContext'); // f2又調用了f3,f3執行完畢以前沒法console 2 ECStack.push('f3 functionContext'); // f3執行完畢,輸出3並出棧 ECStack.pop(); // f2執行完畢,輸出2並出棧 ECStack.pop(); // f1執行完畢,輸出1並出棧 ECStack.pop(); // 此時執行棧中只剩下一個全局執行上下文
那麼到這裏,咱們解釋了執行棧與執行上下文的存儲規則;還記得我在前文提到代碼執行前JS引擎會作準備建立執行上下文嗎,具體怎麼建立呢,咱們接着說。
肆 ❀ 執行上下文建立階段
執行上下文建立分爲建立階段與執行階段兩個階段,較爲難理解應該是建立階段,咱們先說建立階段。
JS執行上下文的建立階段主要負責三件事:肯定this---建立詞法環境(LexicalEnvironment)---建立變量環境(VariableEnvironment)
這裏我就直接借鑑了他人翻譯資料的僞代碼,來表示這個建立過程:
ExecutionContext = { // 肯定this的值 ThisBinding = <this value>, // 建立詞法環境 LexicalEnvironment = {}, // 建立變量環境 VariableEnvironment = {}, };
若是你有閱讀其它關於執行上下文的文章讀到這裏必定有疑問,執行上下文創界過程不是應該解釋this,做用域與變量對象/活動對象纔對嗎,怎麼跟別的地方說的不同,這點我後面解釋。
1.肯定this
官方的稱呼爲This Binding,在全局執行上下文中,this老是指向全局對象,例如瀏覽器環境下this指向window對象。
而在函數執行上下文中,this的值取決於函數的調用方式,若是被一個對象調用,那麼this指向這個對象。不然this通常指向全局對象window或者undefined(嚴格模式)。
我在以前有專門寫一篇介紹this的博文,如今看來寫的很很差,以後我會從新理解寫一篇通俗易懂的文章。
2.詞法環境
詞法環境是一個包含標識符變量映射的結構,這裏的標識符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用。
詞法環境由環境記錄與對外部環境引入記錄兩個部分組成。
其中環境記錄用於存儲變量和函數聲明的實際位置,便於代碼執行階段能對應賦值。而外部環境引入記錄用於保存它能夠訪問的其它外部環境,那麼說到這個,是否是有點做用域鏈的意思?
咱們在前文提到了全局執行上下文與函數執行上下文,因此這也致使了詞法環境分爲全局詞法環境與函數詞法環境兩種。
全局詞法環境:
對外部環境的引入記錄爲null,由於它自己就是最外層環境,除此以外它還包含了全局對象的全部屬性方法,以及用戶自定義的全局對象(經過var聲明)。
函數詞法環境:
包含了用戶在函數中定義的全部變量外,還包含了一個arguments對象。函數詞法環境的外部環境引入能夠是全局環境,也能夠是其它函數環境,這個根據實際代碼而來。
這裏借用譯文中的僞代碼(環境記錄在全局和函數中也不一樣,全局中的環境記錄叫對象環境記錄,函數中環境記錄叫聲明性環境記錄,說多了糊塗,下方有展現):
// 全局環境 GlobalExectionContext = { // 全局詞法環境 LexicalEnvironment: { // 環境記錄 EnvironmentRecord: { Type: "Object", //類型爲對象環境記錄 // 標識符綁定在這裏 }, outer: < null > } }; // 函數環境 FunctionExectionContext = { // 函數詞法環境 LexicalEnvironment: { // 環境紀錄 EnvironmentRecord: { Type: "Declarative", //類型爲聲明性環境記錄 // 標識符綁定在這裏 }, outer: < Global or outerfunction environment reference > } };
3.變量環境
變量環境能夠說也是詞法環境,它具有詞法環境全部屬性,同樣有環境記錄與外部環境引入。在ES6中惟一的區別在於詞法環境用於存儲函數聲明與let const聲明的變量,而變量環境僅僅存儲var聲明的變量。
咱們經過一串僞代碼來理解它們:
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
咱們用僞代碼來描述上述代碼中執行上下文的建立過程:
//全局執行上下文 GlobalExectionContext = { // this綁定爲全局對象 ThisBinding: <Global Object>, // 詞法環境 LexicalEnvironment: { //環境記錄 EnvironmentRecord: { Type: "Object", // 對象環境記錄 // 標識符綁定在這裏 let const建立的變量a b在這 a: < uninitialized >, b: < uninitialized >, multiply: < func > } // 全局環境外部環境引入爲null outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 對象環境記錄 // 標識符綁定在這裏 var建立的c在這 c: undefined, } // 全局環境外部環境引入爲null outer: <null> } } // 函數執行上下文 FunctionExectionContext = { //因爲函數是默認調用 this綁定一樣是全局對象 ThisBinding: <Global Object>, // 詞法環境 LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 聲明性環境記錄 // 標識符綁定在這裏 arguments對象在這 Arguments: {0: 20, 1: 30, length: 2}, }, // 外部環境引入記錄爲</Global> outer: <GlobalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 聲明性環境記錄 // 標識符綁定在這裏 var建立的g在這 g: undefined }, // 外部環境引入記錄爲</Global> outer: <GlobalEnvironment> } }
不知道你有沒有發現,在執行上下文建立階段,函數聲明與var聲明的變量在建立階段已經被賦予了一個值,var聲明被設置爲了undefined,函數被設置爲了自身函數,而let const被設置爲未初始化。
如今你總知道變量提高與函數提高是怎麼回事了吧,以及爲何let const爲何有暫時性死域,這是由於做用域建立階段JS引擎對二者初始化賦值不一樣。
上下文除了建立階段外,還有執行階段,這點你們應該好理解,代碼執行時根據以前的環境記錄對應賦值,好比早期var在建立階段爲undefined,若是有值就對應賦值,像let const值爲未初始化,若是有值就賦值,無值則賦予undefined。
伍 ❀ 關於變量對象與活動對象
回答前面的問題,爲何別人的博文介紹上下文都是談做用域,變量對象和活動對象,我這就成了詞法環境,變量環境了。
我在閱讀相關資料也產生了這個疑問,一番查閱能夠肯定的是,變量對象與活動對象的概念是ES3提出的老概念,從ES5開始就用詞法環境和變量環境替代了,由於更好解釋。
在上文中,咱們經過介紹詞法環境與變量環境解釋了爲何var會存在變量提高,爲何let const沒有,而經過變量對象與活動對象是很難解釋的,由其是在JavaScript在更新中不斷在彌補當初設計的坑。
其次,詞法環境的概念與變量對象這類概念也是能夠對應上的。
咱們知道變量對象與活動對象其實都是變量對象,變量對象是與執行上下文相關的數據做用域,存儲了在上下文中定義的變量和函數聲明。而在函數上下文中,咱們用活動對象(activation object, AO)來表示變量對象。
那這不正好對應到了全局詞法記錄與函數詞法記錄了嗎。並且因爲ES6新增的let const不存在變量提高,因而正好有了詞法環境與變量環境的概念來解釋這個問題。
因此說到這,你也不用爲詞法環境,變量對象的概念鬧衝突了。
咱們來總結下上面提到的概念。
陸 ❀ 總結
1.全局執行上下文通常由瀏覽器建立,代碼執行時就會建立;函數執行上下文只有函數被調用時纔會建立,調用多少次函數就會建立多少上下文。
2.調用棧用於存放全部執行上下文,知足FILO規則。
3.執行上下文建立階段分爲綁定this,建立詞法環境,變量環境三步,二者區別在於詞法環境存放函數聲明與const let聲明的變量,而變量環境只存儲var聲明的變量。
4.詞法環境主要由環境記錄與外部環境引入記錄兩個部分組成,全局上下文與函數上下文的外部環境引入記錄不同,全局爲null,函數爲全局環境或者其它函數環境。環境記錄也不同,全局叫對象環境記錄,函數叫聲明性環境記錄。
5.你應該明白了爲何會存在變量提高,函數提高,而let const沒有。
6.ES3以前的變量對象與活動對象的概念在ES5以後由詞法環境,變量環境來解釋,二者概念不衝突,後者理解更爲通俗易懂。
不得不說相關文章也是看的我心累,也但願對有緣的你有所幫助,那麼到這裏,本文結束。
柒 ❀ 參考