本文3356字,閱讀大約須要9分鐘。
總括: 本文深刻的講解了Javascript中的執行上下文和執行棧。javascript
流水在碰到底處時纔會釋放活力。前端
若是你是或者想成爲一名Javascript開發者,那就必需要知道Javascript內部是如何執行的。正確的理解Javascript中的執行上下文和執行棧對於理解其它Javascript概念(好比變量提高,做用域,閉包等)相當重要。java
正確的去理解Javascript執行上下文和執行棧將會是你成爲一名更好的Javascript開發者。編程
很少廢話,咱們如今就開始:)數組
簡單的來講,執行上下文是一種對Javascript代碼執行環境的一種抽象概念,也就是說只要有Javascript代碼運行,那麼它就必定是運行在執行上下文中。瀏覽器
Javascript一共有三種執行上下文:數據結構
window
對象(瀏覽器環境下),並將this
的值設置爲該全局對象,另一個程序中只能有一個全局上下文。eval
函數中執行的代碼也會有本身的執行上下文,但因爲eval
函數不會被常常用到,這裏就不作討論了。(譯者注:eval
函數容易致使惡意攻擊,而且運行代碼的速度比相應的替代方法慢,由於不推薦使用)。執行棧,在其餘編程語言中也被稱爲「調用棧」,這是一種後進先出(LIFO)的數據結構,被用來儲存在代碼運行階段建立的全部的執行上下文。閉包
當Javascript引擎(譯者注:Javascript引擎是執行Javascript代碼的解釋器,通常被內嵌在瀏覽器中)開始執行你第一行Javascript腳本代碼的時候,它就會建立一個全局執行上下文而後將它壓到執行棧中。每當引擎碰到一個函數的時候,它就會建立一個函數執行上下文,而後將這個執行上下文壓到執行棧中。(譯者注:這種結構相似彈夾,執行棧就是彈夾,執行上下文是子彈,子彈被一個個壓入彈夾,當子彈發射的時候,最後一個進彈夾的子彈會被最早射出)。編程語言
引擎會執行位於執行棧棧頂的執行上下文(通常是函數執行上下文),當該函數執行結束後,對應的執行上下文就會被彈出,而後控制流程到達執行棧的下一個執行上下文。ide
結合下面的代碼來理解下:
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');
<div align="center">上述代碼的執行棧</div>
當上述代碼在瀏覽器中加載時,Javascript引擎首先建立一個全局執行上下文並將其壓入執行棧中,而後碰到first()
函數被調用,此時再建立一個函數執行上下文壓入執行棧中。
當second()
函數在first()
函數中被調用時,引擎再針對這個函數建立一個函數執行上下文將其壓入執行棧中,second
函數執行完畢後,對應的函數執行上下文被推出執行棧銷燬,而後控制流程到下一個執行上下文也就是first
函數。
當first
函數執行結束,first函數執行上下文也被推出,引擎控制流程到全局執行上下文,直到全部的代碼執行完畢,全局執行上下文也會被推出執行棧銷燬,而後程序結束。
如今咱們已經瞭解了Javascript引擎是如何去處理執行上下文的,那麼,執行上下文是如何建立的呢?
執行上下文的建立分爲兩個階段:
執行上下文是在建立階段被建立的,建立階段包括如下幾個方面:
所以執行上下文能夠抽象爲下面的形式:
ExecutionContext = { LexicalEnvironment = <ref. to LexicalEnvironment in memory>, VariableEnvironment = <ref. to VariableEnvironment in memory>, }
ES6的官方文檔 把詞法環境定義爲:
詞法環境(Lexical Environments)是一種規範類型,用於根據ECMAScript代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯。詞法環境由一個環境記錄(Environment Record)和一個可能爲空的外部詞法環境(outer Lexical Environment)引用組成。
簡單來講,詞法環境就是一種標識符—變量映射的結構(這裏的標識符指的是變量/函數的名字,變量是對實際對象[包含函數和數組類型的對象]或基礎數據類型的引用)。
舉個例子,看看下面的代碼:
var a = 20; var b = 40; function foo() { console.log('bar'); }
上面代碼的詞法環境相似這樣:
lexicalEnvironment = { a: 20, b: 40, foo: <ref. to foo function> }
每個詞法環境由下面三部分組成:
this
;所謂的環境記錄就是詞法環境中記錄變量和函數聲明的地方。
環境記錄也有兩種類型:
window
對象)。所以,對於對象的每個新增屬性(對瀏覽器來講,它包含瀏覽器提供給window
對象的全部屬性和方法),都會在該記錄中建立一個新條目。注意:對函數而言,環境記錄還包含一個arguments
對象,該對象是個類數組對象,包含參數索引和參數的映射以及一個傳入函數的參數的長度屬性。舉個例子,一個arguments
對象像下面這樣:
function foo(a, b) { var c = a + b; } foo(2, 3); // argument 對象相似下面這樣 Arguments: { 0: 2, 1: 3, length: 2 }
(譯者注:環境記錄對象在建立階段也被稱爲變量對象(VO),在執行階段被稱爲活動對象(AO)。之因此被稱爲變量對象是由於此時該對象只是存儲執行上下文中變量和函數聲明,以後代碼開始執行,變量會逐漸被初始化或是修改,而後這個對象就被稱爲活動對象)
對於外部環境的引用意味着在當前執行上下文中能夠訪問外部詞法環境。也就是說,若是在當前的詞法環境中找不到某個變量,那麼Javascript引擎會試圖在上層的詞法環境中尋找。(譯者注:Javascript引擎會根據這個屬性來構成咱們常說的做用域鏈)
this
在詞法環境建立階段中,會肯定this
的值。
在全局執行上下文中,this
值會被映射到全局對象中(在瀏覽器中,也就是window
對象)。
在函數執行上下文中,this
值取決於誰調用了該函數,若是是對象調用了它,那麼就將this
值設置爲該對象,不然將this
值設置爲全局對象或是undefined
(嚴格模式下)。例如:
const person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 上面calcAge的 'this' 就是 'person',由於calcAge是被person對象調用的 const calculateAge = person.calcAge; calculateAge(); // 上面的'this' 指向全局對象(window),由於沒有對象調用它,或者說是window調用了它(window省略不寫)
詞法環境抽象出來相似下面的僞代碼:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符在這裏綁定 } outer: <null>, this: <global object> } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符在這裏綁定 } outer: <Global or outer function environment reference>, this: <depends on how function is called> } }
其實變量環境也是詞法環境的一種,它的環境記錄包含了變量聲明語句在執行上下文中建立的變量和具體值的綁定關係。
如上所述,變量環境也是詞法環境的一種,所以它具備詞法環境全部的屬性。
在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", // 標識符在這裏綁定 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符在這裏綁定 c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
在執行階段,將完成變量的賦值操做,所以在執行階段全局執行上下文看起來會像下面這樣:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符在這裏綁定 a: 20, b: 30, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符在這裏綁定 c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
當調用multiply(20, 30)
時,將爲該函數建立一個函數執行上下文,該函數執行上下文在建立階段像下面這樣:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符在這裏綁定 Arguments: { 0: 20, 1: 30, length: 2 }, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符在這裏綁定 g: undefined }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
而後,執行上下文進入執行階段,這時候已經完成了變量的賦值操做。該函數上下文在執行階段像下面這樣:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符在這裏綁定 Arguments: { 0: 20, 1: 30, length: 2 }, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符在這裏綁定 g: 20 }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
函數執行完成後,返回值存儲在變量c
中,此時全局詞法環境被更新。以後,全局代碼執行完成,程序結束。
注意:你可能已經注意到上面代碼,let
和const
定義的變量a
和b
在建立階段沒有被賦值,但var
聲明的變量從在建立階段被賦值爲undefined
。
這是由於,在建立階段,會在代碼中掃描變量和函數聲明,而後將函數聲明存儲在環境中,但變量會被初始化爲undefined
(var
聲明的狀況下)和保持uninitialized
(未初始化狀態)(使用let
和const
聲明的狀況下)。
這就是爲何使用var
聲明的變量能夠在變量聲明以前調用的緣由,但在變量聲明以前訪問使用let
和const
聲明的變量會報錯(TDZ)的緣由。
這其實就是咱們常常聽到的變量聲明提高。
注意:在執行階段,若是Javascript引擎找不到let
和const
聲明的變量的值,也會被賦值爲undefined
。
如上,咱們講解了Javascript代碼是如何執行的,雖說成爲一名優秀的Javascript開發者並不須要徹底搞懂這些概念,但對上面的概念有深刻的理解有助於咱們去學習和理解其它概念,好比:變量聲明提高,閉包,做用域鏈等。
能力有限,水平通常,歡迎勘誤,不勝感激。
訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍