文章來源于公眾號:前端桃園 ,作者桃翁
最近在研究 JavaScript 基礎(chǔ)性的東西,但是看到對于執(zhí)行上下文的解釋我發(fā)現(xiàn)有兩種,一種是執(zhí)行上下文包含:scope
(作用域)、variable object
(變量對象)、this value
(this 值),另外一個種是包含:lexical environment
(詞法環(huán)境)、variable environment
(變量環(huán)境)、this value
(this 值)。
后面我查閱了不少博客以及 ES3 和 ES5 的規(guī)范才了解到,第一種是 ES3 的規(guī)范,經(jīng)典書籍《JavaScript 高級程序設(shè)計》第三版就是這樣解釋的,也是網(wǎng)上廣為流傳的一種,另一種是 ES5 的規(guī)范。
然后我接著又去翻了 ES2018 中的,發(fā)現(xiàn)又有變化了,已經(jīng)增加了更多的內(nèi)容了,考慮到這部分內(nèi)容頗為復雜,準備后面再進行總結(jié)分享,查資料的時候看到這篇講執(zhí)行上下文(ES5 )的還不錯,所以就翻譯出來先分享給大家。
以后看到變量對象、活動對象知道是 ES3 里面的內(nèi)容,而如果是詞法環(huán)境、變量環(huán)境這種詞就是 ES5 以后的內(nèi)容。
以下是正文:
什么是執(zhí)行上下文?
簡而言之,執(zhí)行上下文是計算和執(zhí)行 JavaScript 代碼的環(huán)境的抽象概念。每當 JavaScript 代碼在運行的時候,它都是在執(zhí)行上下文中運行。
執(zhí)行上下文的類型
JavaScript 中有三種執(zhí)行上下文類型。
- 全局執(zhí)行上下文 — 這是默認或者說基礎(chǔ)的上下文,任何不在函數(shù)內(nèi)部的代碼都在全局上下文中。它會執(zhí)行兩件事:創(chuàng)建一個全局的 window 對象(瀏覽器的情況下),并且設(shè)置
this
的值等于這個全局對象。一個程序中只會有一個全局執(zhí)行上下文。 - 函數(shù)執(zhí)行上下文 — 每當一個函數(shù)被調(diào)用時, 都會為該函數(shù)創(chuàng)建一個新的上下文。每個函數(shù)都有它自己的執(zhí)行上下文,不過是在函數(shù)被調(diào)用時創(chuàng)建的。函數(shù)上下文可以有任意多個。每當一個新的執(zhí)行上下文被創(chuàng)建,它會按定義的順序(將在后文討論)執(zhí)行一系列步驟。
- Eval 函數(shù)執(zhí)行上下文 — 執(zhí)行在
eval
函數(shù)內(nèi)部的代碼也會有它屬于自己的執(zhí)行上下文,但由于 JavaScript 開發(fā)者并不經(jīng)常使用eval
,所以在這里我不會討論它。
執(zhí)行棧
執(zhí)行棧,也就是在其它編程語言中所說的“調(diào)用?!?,是一種擁有 LIFO(后進先出)的數(shù)據(jù)結(jié)構(gòu),被用來存儲代碼運行時創(chuàng)建的所有執(zhí)行上下文。
當 JavaScript 引擎第一次遇到你的腳本時,它會創(chuàng)建一個全局的執(zhí)行上下文并且壓入當前執(zhí)行棧。每當引擎遇到一個函數(shù)調(diào)用,它會為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并壓入棧的頂部。
引擎會執(zhí)行處于棧頂?shù)膱?zhí)行上下文的函數(shù)。當該函數(shù)執(zhí)行結(jié)束時,執(zhí)行上下文從棧中彈出,控制流程到達當前棧中的下一個上下文。
讓我們通過下面的代碼示例來理解:
let a = 'Hello World!';
functionfirst() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
functionsecond() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
當上述代碼在瀏覽器加載時,JavaScript 引擎創(chuàng)建了一個全局執(zhí)行上下文并把它壓入當前執(zhí)行棧。當遇到 first()
函數(shù)調(diào)用時,JavaScript 引擎為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并把它壓入當前執(zhí)行棧的頂部。
當從 first()
函數(shù)內(nèi)部調(diào)用 second()
函數(shù)時,JavaScript 引擎為 second()
函數(shù)創(chuàng)建了一個新的執(zhí)行上下文并把它壓入當前執(zhí)行棧的頂部。當 second()
函數(shù)執(zhí)行完畢,它的執(zhí)行上下文會從當前棧彈出,并且控制流程到達下一個執(zhí)行上下文,即 first()
函數(shù)的執(zhí)行上下文。
當 first()
執(zhí)行完畢,它的執(zhí)行上下文從棧彈出,控制流程到達全局執(zhí)行上下文。一旦所有代碼執(zhí)行完畢,JavaScript 引擎從當前棧中移除全局執(zhí)行上下文。
怎么創(chuàng)建執(zhí)行上下文?
到現(xiàn)在,我們已經(jīng)看過 JavaScript 怎樣管理執(zhí)行上下文了,現(xiàn)在讓我們了解 JavaScript 引擎是怎樣創(chuàng)建執(zhí)行上下文的。
創(chuàng)建執(zhí)行上下文有兩個階段:1) 創(chuàng)建階段 和 2) 執(zhí)行階段。
創(chuàng)建階段
在 JavaScript 代碼執(zhí)行前,執(zhí)行上下文將經(jīng)歷創(chuàng)建階段。在創(chuàng)建階段會發(fā)生三件事:
- this 值的決定,即我們所熟知的 this 綁定。
- 創(chuàng)建詞法環(huán)境組件。
- 創(chuàng)建變量環(huán)境組件。
所以執(zhí)行上下文在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
this 綁定:**
在全局執(zhí)行上下文中,this
的值指向全局對象。(在瀏覽器中,this
引用 Window 對象)。
在函數(shù)執(zhí)行上下文中,this
的值取決于該函數(shù)是如何被調(diào)用的。如果它被一個引用對象調(diào)用,那么 this
會被設(shè)置成那個對象,否則 this
的值被設(shè)置為全局對象或者 undefined
(在嚴格模式下)。例如:
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因為 'baz' 被
// 對象 'foo' 調(diào)用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 對象,因為
// 沒有指定引用對象
詞法環(huán)境
官方的 ES6 文檔把詞法環(huán)境定義為
詞法環(huán)境是一種規(guī)范類型,基于 ECMAScript 代碼的詞法嵌套結(jié)構(gòu)來定義標識符和具體變量和函數(shù)的關(guān)聯(lián)。一個詞法環(huán)境由環(huán)境記錄器和一個可能的引用outer詞法環(huán)境的空值組成。
簡單來說詞法環(huán)境是一種持有標識符—變量映射的結(jié)構(gòu)。(這里的標識符指的是變量/函數(shù)的名字,而變量是對實際對象[包含函數(shù)類型對象]或原始數(shù)據(jù)的引用)。
現(xiàn)在,在詞法環(huán)境的內(nèi)部有兩個組件:(1) 環(huán)境記錄器和 (2) 一個外部環(huán)境的引用。
- 環(huán)境記錄器是存儲變量和函數(shù)聲明的實際位置。
- 外部環(huán)境的引用意味著它可以訪問其父級詞法環(huán)境(作用域)。
譯者注:外部環(huán)境已經(jīng)跟 ES3 規(guī)定的作用域的作用類似
詞法環(huán)境有兩種類型:
- 全局環(huán)境(在全局執(zhí)行上下文中)是沒有外部環(huán)境引用的詞法環(huán)境。全局環(huán)境的外部環(huán)境引用是null。它擁有內(nèi)建的 Object/Array/等、在環(huán)境記錄器內(nèi)的原型函數(shù)(關(guān)聯(lián)全局對象,比如 window 對象)還有任何用戶定義的全局變量,并且
this
的值指向全局對象。 - 在函數(shù)環(huán)境中,函數(shù)內(nèi)部用戶定義的變量存儲在環(huán)境記錄器中。并且引用的外部環(huán)境可能是全局環(huán)境,或者任何包含此內(nèi)部函數(shù)的外部函數(shù)。
環(huán)境記錄器也有兩種類型(如上?。?/p>
- 聲明式環(huán)境記錄器存儲變量、函數(shù)和參數(shù)。
- 對象環(huán)境記錄器用來定義出現(xiàn)在全局上下文中的變量和函數(shù)的關(guān)系。
簡而言之,
- 在全局環(huán)境中,環(huán)境記錄器是對象環(huán)境記錄器。
- 在函數(shù)環(huán)境中,環(huán)境記錄器是聲明式環(huán)境記錄器。
注意 — 對于函數(shù)環(huán)境,聲明式環(huán)境記錄器還包含了一個傳遞給函數(shù)的 arguments
對象(此對象存儲索引和參數(shù)的映射)和傳遞給函數(shù)的參數(shù)的 length。
抽象地講,詞法環(huán)境在偽代碼中看起來像這樣:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這里綁定標識符
}
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這里綁定標識符
}
outer: <Global or outer function environment reference>
}
}
變量環(huán)境:
它同樣是一個詞法環(huán)境,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。
如上所述,變量環(huán)境也是一個詞法環(huán)境,所以它有著上面定義的詞法環(huán)境的所有屬性。
在 ES6 中,詞法環(huán)境組件和變量環(huán)境的一個不同就是前者被用來存儲函數(shù)聲明和變量(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);
執(zhí)行上下文看起來像這樣:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這里綁定標識符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這里綁定標識符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這里綁定標識符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這里綁定標識符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意 — 只有遇到調(diào)用函數(shù) multiply
時,函數(shù)執(zhí)行上下文才會被創(chuàng)建。
可能你已經(jīng)注意到 let
和 const
定義的變量并沒有關(guān)聯(lián)任何值,但 var
定義的變量被設(shè)成了undefined
。
這是因為在創(chuàng)建階段時,引擎檢查代碼找出變量和函數(shù)聲明,雖然函數(shù)聲明完全存儲在環(huán)境中,但是變量最初設(shè)置為 undefined
(var
情況下),或者未初始化(let
和 const
情況下)。
這就是為什么你可以在聲明之前訪問 var
定義的變量(雖然是 undefined
),但是在聲明之前訪問 let
和 const
的變量會得到一個引用錯誤。
這就是我們說的變量聲明提升。
執(zhí)行階段
這是整篇文章中最簡單的部分。在此階段,完成對所有這些變量的分配,最后執(zhí)行代碼。
注意 — 在執(zhí)行階段,如果 JavaScript 引擎不能在源碼中聲明的實際位置找到 let
變量的值,它會被賦值為 undefined
。
結(jié)論
我們已經(jīng)討論過 JavaScript 程序內(nèi)部是如何執(zhí)行的。雖然要成為一名卓越的 JavaScript 開發(fā)者并不需要學會全部這些概念,但是如果對上面概念能有不錯的理解將有助于你更輕松,更深入地理解其他概念,如變量聲明提升,作用域和閉包。
以上就是W3Cschool編程獅
關(guān)于(ES5版)深入理解 JavaScript 執(zhí)行上下文和執(zhí)行棧的相關(guān)介紹了,希望對大家有所幫助。