前言
JavaScript
執(zhí)行過程分為兩個(gè)階段,編譯階段和執(zhí)行階段。在編譯階段 JS
引擎主要做了三件事:詞法分析、語法分析和代碼生成;編譯完成后 JS
引擎開始創(chuàng)建執(zhí)行上下文(JavaScript
代碼運(yùn)行的環(huán)境),并執(zhí)行 JS
代碼。
編譯階段
對(duì)于常見編譯型語言(例如:Java
)來說,編譯步驟分為:詞法分析 -> 語法分析 -> 語義檢查 -> 代碼優(yōu)化和字節(jié)碼生成
對(duì)于解釋型語言(例如:JavaScript
)來說,編譯階通過詞法分析 -> 語法分析 -> 代碼生成,就可以解釋并執(zhí)行代碼了。
詞法分析
JS
引擎會(huì)將我們寫的代碼當(dāng)成字符串分解成詞法單元(token)。例如,var a = 2
,這段程序會(huì)被分解成:“var、a、=、2、;” 五個(gè) token
。每個(gè)詞法單元token
不可再分割。可以試試這個(gè)網(wǎng)站地址查看 token
:https://esprima.org/demo/parse.html
1詞法分析1.png
|
1詞法分析2.png
|
---|
語法分析
語法分析階段會(huì)將詞法單元流(數(shù)組),也就是上面所說的token
, 轉(zhuǎn)換成樹狀結(jié)構(gòu)的 “抽象語法樹(AST)”
代碼生成
將AST
轉(zhuǎn)換為可執(zhí)行代碼的過程稱為代碼生成,因?yàn)橛?jì)算機(jī)只能識(shí)別機(jī)器指令,需要通過某種方法將 var a = 2;
的 AST 轉(zhuǎn)化為一組機(jī)器指令,用來創(chuàng)建 a
的變量(包括分配內(nèi)存),并將值存儲(chǔ)在 a
中。
執(zhí)行階段
執(zhí)行程序需要有執(zhí)行環(huán)境, Java
需要 Java
虛擬機(jī),同樣解析 JavaScript
也需要執(zhí)行環(huán)境,我們稱它為“執(zhí)行上下文”。
什么是執(zhí)行上下文
簡(jiǎn)而言之,執(zhí)行上下文是對(duì) JavaScript
代碼執(zhí)行環(huán)境的一種抽象,每當(dāng) JavaScript
運(yùn)行時(shí),它都是在執(zhí)行上下文中運(yùn)行。
執(zhí)行上下文類型
JavaScript
執(zhí)行上下文有三種:
全局執(zhí)行上下文 —— 當(dāng)
JS
引擎執(zhí)行全局代碼的時(shí)候,會(huì)編譯全局代碼并創(chuàng)建執(zhí)行上下文,它會(huì)做兩件事:1、創(chuàng)建一個(gè)全局的window
對(duì)象(瀏覽器環(huán)境下),2、將this
的值設(shè)置為該全局對(duì)象;全局上下文在整個(gè)頁面生命周期有效,并且只有一份。函數(shù)執(zhí)行上下文 —— 當(dāng)調(diào)用一個(gè)函數(shù)的時(shí)候,函數(shù)體內(nèi)的代碼會(huì)被編譯,并創(chuàng)建函數(shù)執(zhí)行上下文,一般情況下,函數(shù)執(zhí)行結(jié)束之后,創(chuàng)建的函數(shù)執(zhí)行上下文會(huì)被銷毀。
eval 執(zhí)行上下文 —— 調(diào)用
eval
函數(shù)也會(huì)創(chuàng)建自己的執(zhí)行上下文(eval函數(shù)容易導(dǎo)致惡意攻擊,并且運(yùn)行代碼的速度比相應(yīng)的替代方法慢,因此不推薦使用)
執(zhí)行棧
執(zhí)行棧這個(gè)概念是比較貼近我們程序員的,學(xué)習(xí)它能讓我們理解 JS
引擎背后工作的原理,開發(fā)中幫助我們調(diào)試代碼,同時(shí)也能應(yīng)對(duì)面試中有關(guān)執(zhí)行棧的面試題。
執(zhí)行棧,在其它編程語言中被叫做“調(diào)用棧”,是一種 LIFO(后進(jìn)先出)棧的數(shù)據(jù)結(jié)構(gòu),被用來存儲(chǔ)代碼運(yùn)行時(shí)創(chuàng)建的所有執(zhí)行上下文。
當(dāng) JS
引擎開始執(zhí)行第一行 JavaScript
代碼時(shí),它會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文然后將它壓到執(zhí)行棧中,每當(dāng)引擎遇到一個(gè)函數(shù)調(diào)用,它會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并壓入棧的頂部。
引擎會(huì)執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)。當(dāng)該函數(shù)執(zhí)行結(jié)束時(shí),執(zhí)行上下文從棧中彈出,控制流程到達(dá)當(dāng)前棧中的下一個(gè)上下文。
結(jié)合下面代碼來理解:
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)上述代碼在瀏覽器加載時(shí),JS
引擎創(chuàng)建了一個(gè)全局執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧。當(dāng)遇到 first()
JS
引擎為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧的頂部。
當(dāng)從 first()
函數(shù)內(nèi)部調(diào)用 second()
JS
引擎為 second()
函數(shù)創(chuàng)建了一個(gè)新的執(zhí)行上下文并把它壓入當(dāng)前執(zhí)行棧的頂部。當(dāng) second()
函數(shù)執(zhí)行完畢,它的執(zhí)行上下文會(huì)從當(dāng)前棧彈出,并且控制流程到達(dá)下一個(gè)執(zhí)行上下文,即 first()
函數(shù)的執(zhí)行上下文。
當(dāng) first()
執(zhí)行完畢,它的執(zhí)行上下文從棧彈出,控制流程到達(dá)全局執(zhí)行上下文。一旦所有代碼執(zhí)行完畢,JavaScript 引擎從當(dāng)前棧中移除全局執(zhí)行上下文。
如何創(chuàng)建執(zhí)行上下文
現(xiàn)在我們已經(jīng)了解了 JS
引擎是如何去管理執(zhí)行上下文的,那么,執(zhí)行上下文是如何創(chuàng)建的呢?
執(zhí)行上下文的創(chuàng)建分為兩個(gè)階段:
- 創(chuàng)建階段;
- 執(zhí)行階段;
創(chuàng)建階段
執(zhí)行上下文創(chuàng)建階段會(huì)做三件事:
- 綁定
this
- 創(chuàng)建詞法環(huán)境
- 創(chuàng)建變量環(huán)境
所以執(zhí)行上下文在概念上表示如下:
ExecutionContext = { // 執(zhí)行上下文
Binding This, // this值綁定
LexicalEnvironment = { ... }, // 詞法環(huán)境
VariableEnvironment = { ... }, // 變量環(huán)境
}
綁定 this
在全局執(zhí)行上下文中,this
的值指向全局對(duì)象。(在瀏覽器中,this
引用 Window
對(duì)象)。
在函數(shù)執(zhí)行上下文中,this 的值取決于該函數(shù)是如何被調(diào)用的
- 通過對(duì)象方法調(diào)用函數(shù),
this
指向調(diào)用的對(duì)象 - 聲明函數(shù)后使用函數(shù)名稱普通調(diào)用,
this
指向全局對(duì)象,嚴(yán)格模式下this
值是undefined
- 使用
new
方式調(diào)用函數(shù),this
指向新創(chuàng)建的對(duì)象 - 使用
call
、apply
、bind
方式調(diào)用函數(shù),會(huì)改變this
的值,指向傳入的第一個(gè)參數(shù),例如
function fn () {
console.log(this)
}
function fn1 () {
'use strict'
console.log(this)
}
fn() // 普通函數(shù)調(diào)用,this 指向window對(duì)象
fn() // 嚴(yán)格模式下,this 值為 undefined
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 指向 'foo'
let bar = foo.baz;
bar(); // 'this' 指向全局 window 對(duì)象,因?yàn)闆]有指定引用對(duì)象
let obj {
name: 'hello'
}
foo.baz.call(obj) // call 改變this值,指向obj對(duì)象
詞法環(huán)境
每一個(gè)詞法環(huán)境由下面兩部分組成:
- 環(huán)境記錄:變量對(duì)象 =》存儲(chǔ)聲明的變量和函數(shù)( let, const, function,函數(shù)參數(shù))
- 外部環(huán)境引用:作用域鏈
ES6的官方文檔 把詞法環(huán)境定義為:
詞法環(huán)境(Lexical Environments)是一種規(guī)范類型,用于根據(jù)ECMAScript代碼的詞法嵌套結(jié)構(gòu)來定義標(biāo)識(shí)符與特定變量和函數(shù)的關(guān)聯(lián)。詞法環(huán)境由一個(gè)環(huán)境記錄(Environment Record)和一個(gè)可能為空的外部詞法環(huán)境(outer Lexical Environment)引用組成。
簡(jiǎn)單來說,詞法環(huán)境就是一種標(biāo)識(shí)符—變量映射的結(jié)構(gòu)(這里的標(biāo)識(shí)符指的是變量/函數(shù)的名字,變量是對(duì)實(shí)際對(duì)象[包含函數(shù)和數(shù)組類型的對(duì)象]或基礎(chǔ)數(shù)據(jù)類型的引用)。
舉個(gè)例子,看看下面的代碼:
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)境中記錄變量和函數(shù)聲明的地方
環(huán)境記錄也有兩種類型:
聲明類環(huán)境記錄。顧名思義,它存儲(chǔ)的是變量和函數(shù)聲明,函數(shù)的詞法環(huán)境內(nèi)部就包含著一個(gè)聲明類環(huán)境記錄。
對(duì)象環(huán)境記錄。全局環(huán)境中的詞法環(huán)境中就包含的就是一個(gè)對(duì)象環(huán)境記錄。除了變量和函數(shù)聲明外,對(duì)象環(huán)境記錄還包括全局對(duì)象(瀏覽器的window
對(duì)象)。因此,對(duì)于對(duì)象的每一個(gè)新增屬性(對(duì)瀏覽器來說,它包含瀏覽器提供給window對(duì)象的所有屬性和方法),都會(huì)在該記錄中創(chuàng)建一個(gè)新條目。
注意:對(duì)函數(shù)而言,環(huán)境記錄還包含一個(gè)arguments對(duì)象,該對(duì)象是個(gè)類數(shù)組對(duì)象,包含參數(shù)索引和參數(shù)的映射以及一個(gè)傳入函數(shù)的參數(shù)的長(zhǎng)度屬性。舉個(gè)例子,一個(gè)arguments對(duì)象像下面這樣:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument 對(duì)象類似下面這樣
Arguments: { 0: 2, 1: 3, length: 2 }
環(huán)境記錄對(duì)象在創(chuàng)建階段也被稱為變量對(duì)象(VO),在執(zhí)行階段被稱為活動(dòng)對(duì)象(AO)。之所以被稱為變量對(duì)象是因?yàn)榇藭r(shí)該對(duì)象只是存儲(chǔ)執(zhí)行上下文中變量和函數(shù)聲明,之后代碼開始執(zhí)行,變量會(huì)逐漸被初始化或是修改,然后這個(gè)對(duì)象就被稱為活動(dòng)對(duì)象
外部環(huán)境引用
對(duì)于外部環(huán)境的引用意味著在當(dāng)前執(zhí)行上下文中可以訪問外部詞法環(huán)境。也就是說,如果在當(dāng)前的詞法環(huán)境中找不到某個(gè)變量,那么Javascript
引擎會(huì)試圖在上層的詞法環(huán)境中尋找。(Javascript引擎會(huì)根據(jù)這個(gè)屬性來構(gòu)成我們常說的作用域鏈)
詞法環(huán)境抽象出來類似下面的偽代碼:
GlobalExectionContext = { // 全局執(zhí)行上下文
this: <global object> // this 值綁定
LexicalEnvironment: { // 全局執(zhí)行上下文詞法環(huán)境
EnvironmentRecord: { // 環(huán)境記錄
Type: "Object",
// 標(biāo)識(shí)符在這里綁定
}
outer: <null> // 外部引用
}
}
FunctionExectionContext = { // 函數(shù)執(zhí)行上下文
this: <depends on how function is called> // this 值綁定
LexicalEnvironment: { // 函數(shù)執(zhí)行上下文詞法環(huán)境
EnvironmentRecord: { // 環(huán)境記錄
Type: "Declarative",
// 標(biāo)識(shí)符在這里綁定
}
outer: <Global or outer function environment reference> // 引用全局環(huán)境
}
}
變量環(huán)境
它同樣是一個(gè)詞法環(huán)境,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。
如上所述,變量環(huán)境也是一個(gè)詞法環(huán)境,所以它有著上面定義的詞法環(huán)境的所有屬性。
在 ES6 中,詞法環(huán)境和變量環(huán)境的一個(gè)不同就是前者被用來存儲(chǔ)函數(shù)聲明和變量(let 和 const)綁定,而后者只用來存儲(chǔ) var 變量綁定。
看點(diǎn)樣例代碼來理解上面的概念:
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",
// 在這里綁定標(biāo)識(shí)符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在這里綁定標(biāo)識(shí)符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這里綁定標(biāo)識(shí)符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在這里綁定標(biāo)識(shí)符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意 — 只有遇到調(diào)用函數(shù) multiply 時(shí),函數(shù)執(zhí)行上下文才會(huì)被創(chuàng)建。
可能你已經(jīng)注意到 let
和 const
定義的變量并沒有關(guān)聯(lián)任何值,但 var
定義的變量被設(shè)成了 undefined
。
這是因?yàn)樵趧?chuàng)建階段時(shí),引擎檢查代碼找出變量和函數(shù)聲明,雖然函數(shù)聲明完全存儲(chǔ)在環(huán)境中,但是變量最初設(shè)置為 undefined
(var
情況下),或者未初始化(let
和 const
情況下)。
這就是為什么你可以在聲明之前訪問 var
定義的變量(雖然是 undefined
),但是在聲明之前訪問 let
和 const
的變量會(huì)得到一個(gè)引用錯(cuò)誤。
這就是我們說的變量聲明提升。
執(zhí)行階段
經(jīng)過上面的創(chuàng)建執(zhí)行上下文,就開始執(zhí)行 JavaScript
代碼了。在執(zhí)行階段,如果 JavaScript
引擎不能在源碼中聲明的實(shí)際位置找到 let
變量的值,它會(huì)被賦值為 undefined
。
執(zhí)行棧應(yīng)用
利用瀏覽器查看棧的調(diào)用信息
我們知道執(zhí)行棧是用來管理執(zhí)行上下文調(diào)用關(guān)系的數(shù)據(jù)結(jié)構(gòu),那么我們?cè)趯?shí)際工作中如何運(yùn)用它呢。
答案是我們可以借助瀏覽器“開發(fā)者工具” source
標(biāo)簽,選擇 JavaScript
代碼打上斷點(diǎn),就可以查看函數(shù)的調(diào)用關(guān)系,并且可以切換查看每個(gè)函數(shù)的變量值
我們?cè)?second
函數(shù)內(nèi)部打上斷點(diǎn),就可以看到右邊 Call Stack
調(diào)用棧顯示 second
、first
、(anonymous)
調(diào)用關(guān)系,second
是在棧頂(anonymous
在棧底相當(dāng)于全局執(zhí)行上下文),執(zhí)行second
函數(shù)我們可以查看該函數(shù)作用域 Scope
局部變量a
、b
和 num
的值,通過查看調(diào)用棧的調(diào)用關(guān)系我們可以快速定位到我們代碼執(zhí)行的情況。
那如果代碼執(zhí)行出錯(cuò),也不知道在哪個(gè)地方打斷點(diǎn)調(diào)試,那怎么查看出錯(cuò)地方的調(diào)用棧呢,告訴大家一個(gè)技巧,如下圖
我們不用打斷點(diǎn),執(zhí)行上面兩步操作,就可以在代碼執(zhí)行異常的地方自動(dòng)打上斷點(diǎn)。知道這個(gè)技巧后,再也不用擔(dān)心代碼出錯(cuò)了。
除了上面通過斷點(diǎn)來查看調(diào)用棧,還可以使用 console.trace() 來輸出當(dāng)前的函數(shù)調(diào)用關(guān)系,比如在示例代碼中的 second
函數(shù)里面加上了 console.trace(),就可以看到控制臺(tái)輸出的結(jié)果,如下圖:
總結(jié)
JavaScript
執(zhí)行分為兩個(gè)階段,編譯階段和執(zhí)行階段。編譯階段會(huì)經(jīng)過詞法分析、語法分析、代碼生成步驟生成可執(zhí)行代碼; JS
引擎執(zhí)行可執(zhí)行性代碼會(huì)創(chuàng)建執(zhí)行上下文,包括綁定this
、創(chuàng)建詞法環(huán)境和變量環(huán)境;詞法環(huán)境創(chuàng)建外部引用(作用域鏈)和 記錄環(huán)境(變量對(duì)象,let, const, function, arguments), JS
引擎創(chuàng)建執(zhí)行上下完成后開始單線程從上到下一行一行執(zhí)行 JS
代碼了。
最后,分享了在開發(fā)過程中一些調(diào)用棧的的應(yīng)用技巧。
引用鏈接
[譯] 理解 JavaScript 中的執(zhí)行上下文和執(zhí)行棧
理解Javascript中的執(zhí)行上下文和執(zhí)行棧
推薦閱讀