寫(xiě)在前頭
最近看執(zhí)行上下文吞滞,一直沒(méi)有找到很好的文章或者書(shū)籍。大多數(shù)都是ES3的舊解釋了扁凛。
執(zhí)行上下文在 ES3 中忍疾,包含三個(gè)部分。
scope:作用域谨朝,也常常被叫做作用域鏈卤妒。
variable object:變量對(duì)象,用于存儲(chǔ)變量的對(duì)象字币。
this value:this 值则披。
在 ES5 中,我們改進(jìn)了命名方式洗出,把執(zhí)行上下文最初的三個(gè)部分改為下面這個(gè)樣子士复。
lexical environment:詞法環(huán)境,當(dāng)獲取變量時(shí)使用翩活。
variable environment:變量環(huán)境阱洪,當(dāng)聲明變量時(shí)使用。
this value:this 值菠镇。
在 ES2018 中冗荸,執(zhí)行上下文又變成了這個(gè)樣子,this 值被歸入 lexical environment利耍,但是增加了不少內(nèi)容蚌本。
lexical environment:詞法環(huán)境,當(dāng)獲取變量或者 this 值時(shí)使用隘梨。
variable environment:變量環(huán)境程癌,當(dāng)聲明變量時(shí)使用
code evaluation state:用于恢復(fù)代碼執(zhí)行位置。
Function:執(zhí)行的任務(wù)是函數(shù)時(shí)使用轴猎,表示正在被執(zhí)行的函數(shù)席楚。
ScriptOrModule:執(zhí)行的任務(wù)是腳本或者模塊時(shí)使用,表示正在被執(zhí)行的代碼税稼。
Realm:使用的基礎(chǔ)庫(kù)和內(nèi)置對(duì)象實(shí)例烦秩。
Generator:僅生成器上下文有這個(gè)屬性,表示當(dāng)前生成器郎仆。
下面是我推薦的不同版本的執(zhí)行上下文的文章
- ES3
冴羽老師的
JavaScript深入之執(zhí)行上下文
- ES5
掘金翻譯計(jì)劃
[譯] 理解 JavaScript 中的執(zhí)行上下文和執(zhí)行棧
- ES2018
原文地址:Understanding Execution Context and Execution Stack in Javascript (要翻墻) 下面的內(nèi)容就是我翻譯的這篇文章(四級(jí)水平加機(jī)翻只祠,yyds)
了解JavaScript程序是如何內(nèi)部執(zhí)行的
如果您是或想成為一名JavaScript開(kāi)發(fā)人員,那么您必須知道JavaScript程序是如何在內(nèi)部執(zhí)行的扰肌。理解執(zhí)行上下文和執(zhí)行堆棧對(duì)于理解其他JavaScript概念(如提升抛寝、作用域和閉包)至關(guān)重要。
正確理解執(zhí)行上下文和執(zhí)行堆棧的概念將使您成為更好的JavaScript開(kāi)發(fā)人員曙旭。
閑話少說(shuō)盗舰,讓我們開(kāi)始吧:)
什么是執(zhí)行上下文?
簡(jiǎn)單地說(shuō),執(zhí)行上下文是評(píng)估和執(zhí)行Javascript代碼的環(huán)境的一個(gè)抽象概念桂躏。任何代碼在JavaScript中運(yùn)行時(shí)钻趋,都在執(zhí)行上下文中運(yùn)行。
執(zhí)行上下文的類型(Types of Execution Context)
在JavaScript中有三種類型的執(zhí)行上下文剂习。
全局執(zhí)行上下文——這是默認(rèn)的或基本的執(zhí)行上下文蛮位。任何不在函數(shù)內(nèi)部的代碼位于全局執(zhí)行上下文中。它執(zhí)行兩件事:它創(chuàng)建一個(gè)全局對(duì)象鳞绕,它是一個(gè)window對(duì)象(在瀏覽器的情況下)失仁,并將this的值設(shè)置為等于全局對(duì)象。一個(gè)程序中只能有一個(gè)全局執(zhí)行上下文们何。
函數(shù)執(zhí)行上下文——每次調(diào)用函數(shù)時(shí)萄焦,都會(huì)為該函數(shù)創(chuàng)建一個(gè)全新的執(zhí)行上下文。每個(gè)函數(shù)都有自己的執(zhí)行上下文冤竹,但它是在調(diào)用或調(diào)用(原文是it’s created when the function is invoked or called)函數(shù)時(shí)創(chuàng)建的拂封。可以有任意數(shù)量的函數(shù)執(zhí)行上下文贴见。每當(dāng)創(chuàng)建一個(gè)新的執(zhí)行上下文時(shí)烘苹,它都會(huì)按照已定義的順序執(zhí)行一系列步驟,我將在本文后面討論這些步驟片部。
Eval函數(shù)執(zhí)行上下文——在
Eval
函數(shù)內(nèi)部執(zhí)行的代碼也會(huì)獲得它自己的執(zhí)行上下文镣衡,但JavaScript開(kāi)發(fā)人員通常不使用Eval
,所以我在這里不討論它档悠。
執(zhí)行棧(Execution Stack)
執(zhí)行棧廊鸥,在其他編程語(yǔ)言中也被稱為“調(diào)用棧”辖所,是一個(gè)具有后進(jìn)先出結(jié)構(gòu)的棧惰说,它用于存儲(chǔ)代碼執(zhí)行期間創(chuàng)建的所有執(zhí)行上下文。
當(dāng)JavaScript引擎第一次遇到腳本時(shí)缘回,它會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文吆视,并將其推入當(dāng)前執(zhí)行棧典挑。每當(dāng)引擎發(fā)現(xiàn)一個(gè)函數(shù)調(diào)用時(shí),它就會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文啦吧,并將其推到棧的頂部您觉。
引擎會(huì)執(zhí)行那些執(zhí)行上下文位于棧頂部的函數(shù)。當(dāng)這個(gè)函數(shù)完成時(shí)授滓,它的執(zhí)行棧從棧中彈出琳水,控件到達(dá)當(dāng)前棧中被彈出的上下文的下面的上下文。
讓我們通過(guò)下面的代碼示例來(lái)理解這一點(diǎ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');
圖片就是上面代碼的執(zhí)行上下文堆棧般堆。
當(dāng)瀏覽器加載上述代碼時(shí)在孝,Javascript引擎會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文,并將其推入當(dāng)前執(zhí)行棧淮摔。當(dāng)遇到對(duì)first()
的調(diào)用時(shí)私沮,Javascript引擎會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文(函數(shù)執(zhí)行上下文),并將其推到當(dāng)前執(zhí)行堆棧的頂部噩咪。
當(dāng)在first()
函數(shù)中調(diào)用second()
函數(shù)時(shí)顾彰,Javascript引擎會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文,并將其推到當(dāng)前執(zhí)行棧的頂部胃碾。當(dāng)second()
函數(shù)結(jié)束時(shí)涨享,它的執(zhí)行上下文從當(dāng)前棧中彈出,控件到達(dá)它下面的執(zhí)行上下文仆百,也就是first()
函數(shù)的執(zhí)行上下文厕隧。
如何創(chuàng)建執(zhí)行上下文?
到目前為止,我們已經(jīng)看到了JavaScript引擎是如何管理執(zhí)行上下文的俄周,現(xiàn)在讓我們來(lái)理解JavaScript引擎是如何創(chuàng)建執(zhí)行上下文的吁讨。
執(zhí)行上下文的創(chuàng)建分為兩個(gè)階段:1)創(chuàng)建階段和2)執(zhí)行階段。
組件創(chuàng)建階段(The Creation Phase)
執(zhí)行上下文在創(chuàng)建階段創(chuàng)建峦朗。在創(chuàng)建階段會(huì)發(fā)生以下事情:
- 創(chuàng)建LexicalEnvironment組件建丧。
- 創(chuàng)建VariableEnvironment組件。
因此波势,執(zhí)行上下文可以在概念上表示為:
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}
詞法環(huán)境(Lexical Environment)
官方ES6文檔將詞匯環(huán)境定義為
(詞法環(huán)境)Lexical Environment是一種規(guī)范類型翎朱,用于根據(jù)ECMAScript代碼的詞法嵌套結(jié)構(gòu)定義標(biāo)識(shí)符與特定變量和函數(shù)的關(guān)聯(lián)。詞法環(huán)境由一個(gè)環(huán)境記錄和一個(gè)可能為空的外部詞匯環(huán)境引用組成尺铣。
簡(jiǎn)單地說(shuō)拴曲,詞法環(huán)境是一個(gè)保存標(biāo)識(shí)符-變量映射的結(jié)構(gòu)。(這里標(biāo)識(shí)符指的是變量/函數(shù)的名稱凛忿,變量是對(duì)實(shí)際對(duì)象[包括函數(shù)對(duì)象和數(shù)組對(duì)象]或原始數(shù)據(jù)的引用)澈灼。
例如,考慮下面的代碼片段:
var a = 20;
var b = 40;
function foo() {
console.log('bar');
}
所以上面代碼片段的詞法環(huán)境是這樣的:
lexicalEnvironment = {
a: 20,
b: 40,
foo: <ref. to foo function>
}
每個(gè)詞法環(huán)境有三個(gè)組成部分:
- Environment Record(環(huán)境記錄器)
- Reference to the outer environment(指向外部環(huán)境的引用)
- This binding. (this綁定)
Environment Record (環(huán)境記錄器)
環(huán)境記錄器是變量和函數(shù)聲明存儲(chǔ)在詞法環(huán)境中的位置。
此外叁熔,環(huán)境記錄器亦有兩類:
聲明性環(huán)境記錄(Declarative environment record)——顧名思義委乌,它存儲(chǔ)變量和函數(shù)聲明。函數(shù)代碼的詞法環(huán)境包含一個(gè)聲明性環(huán)境記錄者疤。
對(duì)象環(huán)境記錄(Object environment record)——全局代碼(global code)的詞法環(huán)境包含一個(gè)客觀環(huán)境記錄(objective environment record)福澡。除了變量和函數(shù)聲明,對(duì)象環(huán)境記錄(the object environment record)還存儲(chǔ)了一個(gè)全局綁定對(duì)象(瀏覽器中的window對(duì)象)驹马。因此,對(duì)于每個(gè)綁定對(duì)象的屬性(在瀏覽器中除秀,它包含瀏覽器提供給window對(duì)象的屬性和方法)糯累,記錄中會(huì)創(chuàng)建一個(gè)新條目(new entry)。
注意:對(duì)于函數(shù)代碼(function code)册踩,環(huán)境記錄還包含一個(gè)參數(shù)對(duì)象(argument對(duì)象)泳姐,該對(duì)象包含傳遞給函數(shù)的索引和參數(shù)之間的映射,以及傳遞給函數(shù)的參數(shù)的長(zhǎng)度(數(shù)量)暂吉。例如胖秒,下面函數(shù)的參數(shù)對(duì)象是這樣的:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
Reference to the Outer Environment(指向外部環(huán)境的引用)
Reference to the Outer Environment指的是它能夠接觸到外部的詞法環(huán)境。這意味著慕的,如果在當(dāng)前詞法環(huán)境中沒(méi)有找到想要查找的變量阎肝,JavaScript引擎可以在外部環(huán)境中查找它們。
This Binding (this綁定)
在此組件中肮街,this的值被確定或設(shè)置(determined or set)风题。
在全局執(zhí)行上下文中,this的值指向全局對(duì)象嫉父。(在瀏覽器中沛硅,它指的是Window對(duì)象)。
在函數(shù)執(zhí)行上下文中绕辖,this的值取決于函數(shù)的調(diào)用方式摇肌。如果它是通過(guò)對(duì)象引用調(diào)用的,那么this的值被設(shè)置為該對(duì)象仪际,否則围小,this的值被設(shè)置為全局對(duì)象或未定義(在嚴(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
// 'this'指的是'person'弟头,因?yàn)?calcAge'是用'person'對(duì)象引用調(diào)用的
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given
// 'this'引用全局window對(duì)象吩抓,因?yàn)闆](méi)有給出對(duì)象引用
抽象地說(shuō),偽代碼中的詞法環(huán)境是這樣的:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
// 標(biāo)識(shí)符綁定到這里
}
outer: <null>,
this: <global object>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
// 標(biāo)識(shí)符綁定到這里
}
outer: <Global or outer function environment reference>,
this: <depends on how function is called>
}
}
變量環(huán)境 (Variable Environment)
它也是一個(gè)詞法環(huán)境赴恨,它的環(huán)境記錄器(EnvironmentRecord)保存由VariableStatements 在執(zhí)行上下文中創(chuàng)建的綁定疹娶。
如上所述,變量環(huán)境也是一個(gè)詞法環(huán)境伦连,因此它具有上述定義的詞法環(huán)境的所有屬性和組件雨饺。
在ES6中钳垮,詞法環(huán)境(LexicalEnvironment)組件和變量環(huán)境(VariableEnvironment)組件之間的一個(gè)區(qū)別是,前者用于存儲(chǔ)函數(shù)聲明和變量(let和const)綁定额港,而后者僅用于存儲(chǔ)變量(var)綁定饺窿。
執(zhí)行程序階段(Execution Phase)
在這個(gè)階段,所有這些變量的賦值都完成了移斩,代碼也最終執(zhí)行了肚医。
Example (例子)
讓我們看一些例子來(lái)理解上述概念:
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í)行上述代碼時(shí)(the above code is executed),JavaScript引擎創(chuàng)建一個(gè)全局執(zhí)行上下文來(lái)執(zhí)行全局代碼向瓷。所以在創(chuàng)建階段肠套,全局執(zhí)行上下文看起來(lái)像這樣:
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 >
}
}
在執(zhí)行階段(During the execution phase),完成變量賦值猖任。因此你稚,在執(zhí)行階段,全局執(zhí)行上下文將類似于以下內(nèi)容朱躺。
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)遇到對(duì)function multiply(20,30)
的調(diào)用時(shí)刁赖,將創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文來(lái)執(zhí)行函數(shù)代碼。所以在創(chuàng)建階段长搀,函數(shù)執(zhí)行上下文看起來(lái)像這樣:
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í)行上下文將經(jīng)歷執(zhí)行階段(the execution phase),這意味著完成對(duì)函數(shù)內(nèi)變量的賦值盈滴。所以在執(zhí)行階段涯肩,函數(shù)的執(zhí)行上下文看起來(lái)像這樣:
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ù)完成后,返回值被存儲(chǔ)在c
中巢钓。因此全局詞法環(huán)境被更新病苗。之后,全局代碼完成症汹,程序結(jié)束硫朦。
注意——你可能已經(jīng)注意到let
和const
定義的變量在創(chuàng)建階段沒(méi)有任何關(guān)聯(lián)的值,但是var
定義的變量被設(shè)置為undefined
背镇。
這是因?yàn)橐д梗趧?chuàng)建階段,代碼被掃描以查找變量和函數(shù)聲明瞒斩,而函數(shù)聲明被完整地存儲(chǔ)在環(huán)境中破婆,變量最初被設(shè)置為未定義(對(duì)于var
)或保持未初始化(對(duì)于let
和const
)。
這就是為什么你可以在聲明之前訪問(wèn)var
定義的變量(雖然未定義)胸囱,但在聲明之前訪問(wèn)let
和const
變量時(shí)會(huì)得到引用錯(cuò)誤的原因祷舀。
這就是我們所說(shuō)的變量提升(hoisting)。
注意:在執(zhí)行階段,如果JavaScript引擎無(wú)法在源代碼中聲明let
變量的實(shí)際位置找到它的值裳扯,那么它將給它賦值為undefined
抛丽。
總結(jié)(Conclusion)
我們已經(jīng)討論了JavaScript程序是如何在內(nèi)部執(zhí)行的。雖然要成為出色的JavaScript開(kāi)發(fā)人員并不需要學(xué)習(xí)所有這些概念饰豺,但充分理解上述概念將有助于您更容易亿鲜、更深入地理解其他概念,如變量聲明提升(hoisting)冤吨、作用域(Scope)和閉包(Closures)蒿柳。
就是這樣,如果你覺(jué)得這篇文章有幫助锅很,請(qǐng)點(diǎn)擊??按鈕其馏,并隨時(shí)在下面發(fā)表評(píng)論!我很樂(lè)意和??交流
本文由mdnice多平臺(tái)發(fā)布