若是你想成爲一個Javascript開發者,那麼你必定要知道Javascript程序的內部運行原理。理解執行環境和執行棧是很是重要的,其有助於理解其餘Javascript的概念,好比說提高,做用域和閉包等。javascript
固然,理解執行環境和執行棧的概念也將會使你成爲一個更好的Javascript開發者。java
閒話少說,立刻開始吧。編程
簡單來講,執行環境就是Javascript代碼被計算和執行的環境的一個抽象概念。不管Javascript代碼在何時運行,它都會運行在 執行環境中。windows
在Javascript中有三種執行環境的類型。數組
全局執行環境 - 這是一種默認和基礎的執行環境。若是代碼不在任何的函數中,那麼它就是在全局執行環境中。他作了兩件事情:首先,它建立了一個全局對象 - windows(若是是瀏覽器的話),而且把this的值設置到全局對象中。在程序中,只會存在一個全局執行環境。瀏覽器
函數執行環境 - 每次當函數被調用的時候,就會爲該函數建立一個全新的執行環境。每一個函數都有他們本身的執行環境,可是他們僅僅是在函數被調用的時候纔會被建立。其能夠有任意多個函數執行環境。不管新的執行環境在何時被建立,它都會按照定義的順序依次執行一系列的步驟,不過這些咱們稍後會講。閉包
eval函數執行環境 - 在eval函數中執行代碼也會得到它本身的執行環境,可是eval並不常常被Javascript開發者所使用,因此這裏咱們目前並不打算討論它。編程語言
執行棧,在其餘編程語言中也被稱爲調用棧,它是一種LIFO(後進先出)的結構,被用於在代碼執行階段存儲全部建立過的執行環境。ide
當Javascript引擎首次運行到你的腳本時,它會建立一個全局執行環境,並把它推入到當前的執行棧中。每當引擎運行到其函數調用時,就會爲這個函數建立一個新的執行環境,並把它推入到堆棧的頂部。函數
引擎會執行其執行環境位於堆棧頂部的函數。當函數執行完畢時,當前執行棧會從堆棧中彈出去,而且控件將會到達其在當前堆棧下面的那個執行環境中。
咱們來經過下面的代碼示例來理解:
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
當上面的代碼加載到瀏覽器中時,Javascript引擎會建立一個全局執行環境,並把它推到當前的執行棧中。當遇到對first()的調用時,Javascript引擎會爲這個函數建立一個新的執行環境,而且把它推到當前執行棧的頂部。
當second()函數在first()函數內被調用時,Javascript引擎會爲這個函數建立一個新的執行環境,並把它推送到當前執行棧的頂部。當second()函數完成的時候,它的執行環境會從當前的棧中推出去,而且空間會到達當前環境下面的那個執行環境中,也就是first()函數執行環境。
當first()完成之後,它的執行環境會會從堆棧中移出,而且控件會到達全局執行環境。當全部代碼執行完之後,Javascript引擎會從當前棧中移出全局執行環境。
那麼執行環境是如何被建立出來的呢?
到如今爲止,咱們已經看到Javascript引擎是如何管理執行環境的。那麼如今我們來理解一下執行環境是如何被Javascript引擎建立出來的吧。
執行環境的建立過程分爲兩個階段:1,建立階段,2,執行階段。
執行環境是在建立階段被建立出來的。在建立階段會發生下面的事情:
詞法環境組件被建立出來。
變量環境組件被建立出來。
所以執行環境從概念上能夠被表示爲:
ExecutionContext = { LexicalEnvironment = <ref. to LexicalEnvironment in memory>, VariableEnvironment = <ref. to VariableEnvironment in memory>, }
官方ES6文檔定義的詞法環境以下:
詞法環境是一種規範類型,用於根據ECMAScript代碼的詞法嵌套結構定義標識符與特定變量和函數的關聯。詞法環境由環境記錄和一個對外部詞彙環境的可能的空引用組成。
簡單來講,詞法環境是一個保存「變量-標識符」映射的結構。(標識符指向變量/函數的名稱,變量是實際對象【包括函數對象和數組對象】的引用,或者是原始值)
例如,思考下面的代碼片斷:
var a = 20; var b = 40; function foo() { console.log('bar'); }
上面的代碼片斷的詞法環境以下:
lexicalEnvironment = { a: 20, b: 40, foo: <ref. to foo function> }
每個詞法環境都有三組件:
環境記錄
對外層環境的引用
this綁定
環境記錄是變量和函數聲明的地方,其被存儲在詞法環境內部。
有兩種詞法環境的類型:
聲明環境記錄 - 顧名思義,它存儲變量和函數的聲明。函數代碼的詞法環境包含一個聲明環境記錄。
對象環境記錄 - 全局代碼的詞法環境包含一個對象環境記錄。除了變量和函數聲明以外,對象環境記錄也會存儲全局綁定對象(瀏覽器中的window對象)。所以對於每一個綁定對象的屬性(對於瀏覽器,它包含全部由瀏覽器給window對象的屬性和方法),在記錄中建立一個新的條目。
注意 - 對於函數代碼,環境記錄也會包含參數對象,參數對象包含傳遞給函數的參數以及索引,和傳遞給函數的參數的長度(個數)。例如,下面函數的參數對象看起來像這樣子的:
function foo(a, b) { var c = a + b; } foo(2, 3); // argument object Arguments: {0: 2, 1: 3, length: 2},
對外部環境的引用意味着它能夠訪問外面的詞法環境。這意味着若是他們在當前的詞法環境中沒有找到的話,Javascript引擎會在外面的環境裏去尋找變量。
在這個組件中,this的值是肯定的或者是已經設置的。
在全局執行環境中,this的值指向全局對象。(在瀏覽器中,this指向window對象)
在函數執行環境中,this的值依賴於函數的調用方式。若是它是在對象引用中被調用,this的值就被設置爲那個對象,不然,this的值會被設置爲全局對象或者是undefined(在嚴格模式中)。例如:
const person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 'this' refers to 'person', because 'calcAge' was called with //'person' object reference const calculateAge = person.calcAge; calculateAge(); // 'this' refers to the global window object, because no object reference was given
抽象的說,在僞代碼中,詞法環境看起來像這樣:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here } outer: <null>, this: <global object> } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here } outer: <Global or outer function environment reference>, this: <depends on how function is called> } }
它也是一個詞法環境,其環境記錄中環境記錄保存着在運行環境中的VariableStatements建立的綁定。
正如上面所寫的,變量環境也是一個詞法環境,所以他有如上定義的詞法環境的全部的屬性和組件。
在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);
當上面的代碼被執行的時候,Javascript引擎會建立一個全局的執行環境來執行這些全局代碼。所以全局執行環境在建立階段看起來像這樣子的:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
在運行階段,變量賦值已經完成。所以全局執行環境在執行階段看起來就像是這樣的:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here a: 20, b: 30, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
當遇到函數multiply(20,30)的調用時,一個新的函數執行環境被建立並執行函數中的代碼。所以函數執行環境在建立階段看起來像是這樣子的:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here g: undefined }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
在這之後,執行環境會經歷執行階段,這意味着在函數內部賦值給變量的過程已經完成。所以此函數執行環境在執行階段看起來就像這樣的:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here g: 20 }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
在函數執行完成之後,返回值會被存儲在c裏。所以全局詞法環境被更新。在這以後,全局代碼執行完成,程序運行終止。
注意:正如你所注意到的,let和const在建立階段定義的變量沒有值與他們相關聯,可是var定義變量會設置爲false。
這是由於,在建立階段,掃描代碼以查找變量和函數聲明,當函數定義被所有存儲到環境中時,變量首先會被初始化爲undefined(在var的狀況中),或者保持未初始化狀態(在let和const的狀況中)。
這就是你在他們定義以前(雖然是undefined)訪問var定義的變量,可是當你在定義以前訪問let和const定義的變量時,會獲得一個引用錯誤。
這就是咱們所謂的提高。
注意 - 在執行階段,若是javascript引擎在源代碼中聲明的實際位置找不到let變量的值,那麼它將爲其分配未定義的值。
因此咱們已經討論瞭如何在內部執行JavaScript程序。 雖然您沒有必要將全部這些概念都學習成爲一名出色的JavaScript開發人員,但對上述概念有一個正確的理解將有助於您更輕鬆,更深刻地理解其餘概念,如提高,做用域和閉包。
翻譯自: