深入理解Javascript中的執(zhí)行環(huán)境(Execution Context)和執(zhí)行棧(Execution Stack)

如果你想成為一個Javascript開發(fā)者,那么你一定要知道Javascript程序的內(nèi)部運行原理。理解執(zhí)行環(huán)境和執(zhí)行棧是非常重要的,其有助于理解其他Javascript的概念,比如說提升,作用域和閉包等。

當(dāng)然,理解執(zhí)行環(huán)境和執(zhí)行棧的概念也將會使你成為一個更好的Javascript開發(fā)者。

閑話少說,馬上開始吧。

## 執(zhí)行環(huán)境是什么

簡單來說,執(zhí)行環(huán)境就是Javascript代碼被計算和執(zhí)行的環(huán)境的一個抽象概念。無論Javascript代碼在什么時候運行,它都會運行在 執(zhí)行環(huán)境中。

## 執(zhí)行環(huán)境的類型

在Javascript中有三種執(zhí)行環(huán)境的類型。

全局執(zhí)行環(huán)境 - 這是一種默認(rèn)和基礎(chǔ)的執(zhí)行環(huán)境。如果代碼不在任何的函數(shù)中,那么它就是在全局執(zhí)行環(huán)境中。他做了兩件事情:首先,它創(chuàng)建了一個全局對象 - windows(如果是瀏覽器的話),并且把this的值設(shè)置到全局對象中。在程序中,只會存在一個全局執(zhí)行環(huán)境。

函數(shù)執(zhí)行環(huán)境 - 每次當(dāng)函數(shù)被調(diào)用的時候,就會為該函數(shù)創(chuàng)建一個全新的執(zhí)行環(huán)境。每個函數(shù)都有他們自己的執(zhí)行環(huán)境,但是他們僅僅是在函數(shù)被調(diào)用的時候才會被創(chuàng)建。其可以有任意多個函數(shù)執(zhí)行環(huán)境。無論新的執(zhí)行環(huán)境在什么時候被創(chuàng)建,它都會按照定義的順序依次執(zhí)行一系列的步驟,不過這些我們稍后會講。

eval函數(shù)執(zhí)行環(huán)境 - 在eval函數(shù)中執(zhí)行代碼也會獲得它自己的執(zhí)行環(huán)境,但是eval并不經(jīng)常被Javascript開發(fā)者所使用,所以這里我們目前并不打算討論它。

## 執(zhí)行棧

執(zhí)行棧,在其他編程語言中也被稱為調(diào)用棧,它是一種LIFO(后進(jìn)先出)的結(jié)構(gòu),被用于在代碼執(zhí)行階段存儲所有創(chuàng)建過的執(zhí)行環(huán)境。

當(dāng)Javascript引擎首次運行到你的腳本時,它會創(chuàng)建一個全局執(zhí)行環(huán)境,并把它推入到當(dāng)前的執(zhí)行棧中。每當(dāng)引擎運行到其函數(shù)調(diào)用時,就會為這個函數(shù)創(chuàng)建一個新的執(zhí)行環(huán)境,并把它推入到堆棧的頂部。

引擎會執(zhí)行其執(zhí)行環(huán)境位于堆棧頂部的函數(shù)。當(dāng)函數(shù)執(zhí)行完畢時,當(dāng)前執(zhí)行棧會從堆棧中彈出去,并且控件將會到達(dá)其在當(dāng)前堆棧下面的那個執(zhí)行環(huán)境中。

我們來通過下面的代碼示例來理解:

? ? 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');



當(dāng)上面的代碼加載到瀏覽器中時,Javascript引擎會創(chuàng)建一個全局執(zhí)行環(huán)境,并把它推到當(dāng)前的執(zhí)行棧中。當(dāng)遇到對first()的調(diào)用時,Javascript引擎會為這個函數(shù)創(chuàng)建一個新的執(zhí)行環(huán)境,并且把它推到當(dāng)前執(zhí)行棧的頂部。

當(dāng)second()函數(shù)在first()函數(shù)內(nèi)被調(diào)用時,Javascript引擎會為這個函數(shù)創(chuàng)建一個新的執(zhí)行環(huán)境,并把它推送到當(dāng)前執(zhí)行棧的頂部。當(dāng)second()函數(shù)完成的時候,它的執(zhí)行環(huán)境會從當(dāng)前的棧中推出去,并且空間會到達(dá)當(dāng)前環(huán)境下面的那個執(zhí)行環(huán)境中,也就是first()函數(shù)執(zhí)行環(huán)境。

當(dāng)first()完成以后,它的執(zhí)行環(huán)境會會從堆棧中移出,并且控件會到達(dá)全局執(zhí)行環(huán)境。當(dāng)所有代碼執(zhí)行完以后,Javascript引擎會從當(dāng)前棧中移出全局執(zhí)行環(huán)境。

那么執(zhí)行環(huán)境是如何被創(chuàng)建出來的呢?

到現(xiàn)在為止,我們已經(jīng)看到Javascript引擎是如何管理執(zhí)行環(huán)境的。那么現(xiàn)在咱們來理解一下執(zhí)行環(huán)境是如何被Javascript引擎創(chuàng)建出來的吧。

執(zhí)行環(huán)境的創(chuàng)建過程分為兩個階段:1,創(chuàng)建階段,2,執(zhí)行階段。

## 創(chuàng)建階段

執(zhí)行環(huán)境是在創(chuàng)建階段被創(chuàng)建出來的。在創(chuàng)建階段會發(fā)生下面的事情:

詞法環(huán)境組件被創(chuàng)建出來。

變量環(huán)境組件被創(chuàng)建出來。

因此執(zhí)行環(huán)境從概念上可以被表示為:

? ? ExecutionContext = {

? ? ? LexicalEnvironment = <ref. to LexicalEnvironment in memory>,

? ? ? VariableEnvironment = <ref. to VariableEnvironment in? memory>,

? ? }

## 詞法環(huán)境

官方ES6文檔定義的詞法環(huán)境如下:

詞法環(huán)境是一種規(guī)范類型,用于根據(jù)ECMAScript代碼的詞法嵌套結(jié)構(gòu)定義標(biāo)識符與特定變量和函數(shù)的關(guān)聯(lián)。詞法環(huán)境由環(huán)境記錄和一個對外部詞匯環(huán)境的可能的空引用組成。

簡單來說,詞法環(huán)境是一個保存“變量-標(biāo)識符”映射的結(jié)構(gòu)。(標(biāo)識符指向變量/函數(shù)的名稱,變量是實際對象【包括函數(shù)對象和數(shù)組對象】的引用,或者是原始值)

例如,思考下面的代碼片段:

? ? var a = 20;

? ? var b = 40;

? ? function foo() {

? ? ? console.log('bar');

? ? }

上面的代碼片段的詞法環(huán)境如下:

? ? lexicalEnvironment = {

? ? ? a: 20,

? ? ? b: 40,

? ? ? foo: <ref. to foo function>

? ? }

每一個詞法環(huán)境都有三組件:

環(huán)境記錄

對外層環(huán)境的引用

this綁定

## 環(huán)境記錄

環(huán)境記錄是變量和函數(shù)聲明的地方,其被存儲在詞法環(huán)境內(nèi)部。

有兩種詞法環(huán)境的類型:

聲明環(huán)境記錄 - 顧名思義,它存儲變量和函數(shù)的聲明。函數(shù)代碼的詞法環(huán)境包含一個聲明環(huán)境記錄。

對象環(huán)境記錄 - 全局代碼的詞法環(huán)境包含一個對象環(huán)境記錄。除了變量和函數(shù)聲明之外,對象環(huán)境記錄也會存儲全局綁定對象(瀏覽器中的window對象)。因此對于每個綁定對象的屬性(對于瀏覽器,它包含所有由瀏覽器給window對象的屬性和方法),在記錄中創(chuàng)建一個新的條目。

注意 - 對于函數(shù)代碼,環(huán)境記錄也會包含參數(shù)對象,參數(shù)對象包含傳遞給函數(shù)的參數(shù)以及索引,和傳遞給函數(shù)的參數(shù)的長度(個數(shù))。例如,下面函數(shù)的參數(shù)對象看起來像這樣子的:

? ? function foo(a, b) {

? ? ? var c = a + b;

? ? }

? ? foo(2, 3);

? ? // argument object

? ? Arguments: {0: 2, 1: 3, length: 2},

## 對外部環(huán)境的引用

對外部環(huán)境的引用意味著它可以訪問外面的詞法環(huán)境。這意味著如果他們在當(dāng)前的詞法環(huán)境中沒有找到的話,Javascript引擎會在外面的環(huán)境里去尋找變量。

## this綁定

在這個組件中,this的值是確定的或者是已經(jīng)設(shè)置的。

在全局執(zhí)行環(huán)境中,this的值指向全局對象。(在瀏覽器中,this指向window對象)

在函數(shù)執(zhí)行環(huán)境中,this的值依賴于函數(shù)的調(diào)用方式。如果它是在對象引用中被調(diào)用,this的值就被設(shè)置為那個對象,否則,this的值會被設(shè)置為全局對象或者是undefined(在嚴(yán)格模式中)。例如:

? ? 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

抽象的說,在偽代碼中,詞法環(huán)境看起來像這樣:

? ? 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>

? ? ? }

? ? }

## 變量環(huán)境:

它也是一個詞法環(huán)境,其環(huán)境記錄中環(huán)境記錄保存著在運行環(huán)境中的VariableStatements創(chuàng)建的綁定。

正如上面所寫的,變量環(huán)境也是一個詞法環(huán)境,因此他有如上定義的詞法環(huán)境的所有的屬性和組件。

在ES6中,詞法環(huán)境組件和變量環(huán)境組件的一個不同點就是前者被用于存儲函數(shù)聲明和變量(let,const)的綁定。而后者只被用于存儲變量(var)的綁定。

## 執(zhí)行階段

在這個階段,所有的變量賦值都會完成,所有的代碼最終也都會執(zhí)行完畢。

## 例子

我們來看一些例子來理解上面的概念。

? ? let a = 20;

? ? const b = 30;

? ? var c;

? ? function multiply(e, f) {

? ? ? var g = 20;

? ? ? return e * f * g;

? ? }

? ? c = multiply(20, 30);

當(dāng)上面的代碼被執(zhí)行的時候,Javascript引擎會創(chuàng)建一個全局的執(zhí)行環(huán)境來執(zhí)行這些全局代碼。因此全局執(zhí)行環(huán)境在創(chuàng)建階段看起來像這樣子的:

? ? 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>

? ? ? }

? ? }

在運行階段,變量賦值已經(jīng)完成。因此全局執(zhí)行環(huán)境在執(zhí)行階段看起來就像是這樣的:

? ? 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>

? ? ? }

? ? }

當(dāng)遇到函數(shù)multiply(20,30)的調(diào)用時,一個新的函數(shù)執(zhí)行環(huán)境被創(chuàng)建并執(zhí)行函數(shù)中的代碼。因此函數(shù)執(zhí)行環(huán)境在創(chuàng)建階段看起來像是這樣子的:

? ? 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>

? ? ? }

? ? }

在這以后,執(zhí)行環(huán)境會經(jīng)歷執(zhí)行階段,這意味著在函數(shù)內(nèi)部賦值給變量的過程已經(jīng)完成。因此此函數(shù)執(zhí)行環(huán)境在執(zhí)行階段看起來就像這樣的:

? ? 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>

? ? ? }

? ? }

在函數(shù)執(zhí)行完成以后,返回值會被存儲在c里。因此全局詞法環(huán)境被更新。在這之后,全局代碼執(zhí)行完成,程序運行終止。

注意:正如你所注意到的,let和const在創(chuàng)建階段定義的變量沒有值與他們相關(guān)聯(lián),但是var定義變量會設(shè)置為false。

這是因為,在創(chuàng)建階段,掃描代碼以查找變量和函數(shù)聲明,當(dāng)函數(shù)定義被全部存儲到環(huán)境中時,變量首先會被初始化為undefined(在var的情況中),或者保持未初始化狀態(tài)(在let和const的情況中)。

這就是你在他們定義之前(雖然是undefined)訪問var定義的變量,但是當(dāng)你在定義之前訪問let和const定義的變量時,會得到一個引用錯誤。

這就是我們所謂的提升。

注意 - 在執(zhí)行階段,如果javascript引擎在源代碼中聲明的實際位置找不到let變量的值,那么它將為其分配未定義的值。

## 結(jié)論

所以我們已經(jīng)討論了如何在內(nèi)部執(zhí)行JavaScript程序。 雖然您沒有必要將所有這些概念都學(xué)習(xí)成為一名出色的JavaScript開發(fā)人員,但對上述概念有一個正確的理解將有助于您更輕松,更深入地理解其他概念,如提升,作用域和閉包。

翻譯自:

https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

轉(zhuǎn)載自:http://www.lht.ren/article/18/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容