前言
JavaScript
執(zhí)行過程分為兩個階段源哩,編譯階段和執(zhí)行階段兴革。在編譯階段 JS
引擎主要做了三件事:詞法分析趣席、語法分析和代碼生成;編譯完成后 JS
引擎開始創(chuàng)建執(zhí)行上下文(JavaScript
代碼運行的環(huán)境)汤功,并執(zhí)行 JS
代碼。
編譯階段
對于常見編譯型語言(例如:Java
)來說溜哮,編譯步驟分為:詞法分析 -> 語法分析 -> 語義檢查 -> 代碼優(yōu)化和字節(jié)碼生成
對于解釋型語言(例如:JavaScript
)來說滔金,編譯階通過詞法分析 -> 語法分析 -> 代碼生成,就可以解釋并執(zhí)行代碼了茂嗓。
詞法分析
JS
引擎會將我們寫的代碼當成字符串分解成詞法單元(token)餐茵。例如,var a = 2
述吸,這段程序會被分解成:“var忿族、a、=蝌矛、2道批、;” 五個 token
朴读。每個詞法單元token
不可再分割屹徘。可以試試這個網(wǎng)站地址查看 token
:https://esprima.org/demo/parse.html
語法分析
語法分析階段會將詞法單元流(數(shù)組)衅金,也就是上面所說的token
, 轉(zhuǎn)換成樹狀結構的 “抽象語法樹(AST)”
代碼生成
將AST
轉(zhuǎn)換為可執(zhí)行代碼的過程稱為代碼生成噪伊,因為計算機只能識別機器指令簿煌,需要通過某種方法將 var a = 2;
的 AST 轉(zhuǎn)化為一組機器指令,用來創(chuàng)建 a
的變量(包括分配內(nèi)存)鉴吹,并將值存儲在 a
中姨伟。
執(zhí)行階段
執(zhí)行程序需要有執(zhí)行環(huán)境, Java
需要 Java
虛擬機豆励,同樣解析 JavaScript
也需要執(zhí)行環(huán)境夺荒,我們稱它為“執(zhí)行上下文”。
什么是執(zhí)行上下文
簡而言之良蒸,執(zhí)行上下文是對 JavaScript
代碼執(zhí)行環(huán)境的一種抽象技扼,每當 JavaScript
運行時,它都是在執(zhí)行上下文中運行嫩痰。
執(zhí)行上下文類型
JavaScript
執(zhí)行上下文有三種:
全局執(zhí)行上下文 —— 當
JS
引擎執(zhí)行全局代碼的時候剿吻,會編譯全局代碼并創(chuàng)建執(zhí)行上下文,它會做兩件事:1串纺、創(chuàng)建一個全局的window
對象(瀏覽器環(huán)境下)丽旅,2、將this
的值設置為該全局對象纺棺;全局上下文在整個頁面生命周期有效榄笙,并且只有一份。函數(shù)執(zhí)行上下文 —— 當調(diào)用一個函數(shù)的時候祷蝌,函數(shù)體內(nèi)的代碼會被編譯茅撞,并創(chuàng)建函數(shù)執(zhí)行上下文,一般情況下巨朦,函數(shù)執(zhí)行結束之后乡翅,創(chuàng)建的函數(shù)執(zhí)行上下文會被銷毀。
eval 執(zhí)行上下文 —— 調(diào)用
eval
函數(shù)也會創(chuàng)建自己的執(zhí)行上下文(eval函數(shù)容易導致惡意攻擊罪郊,并且運行代碼的速度比相應的替代方法慢蠕蚜,因此不推薦使用)
執(zhí)行棧
執(zhí)行棧這個概念是比較貼近我們程序員的,學習它能讓我們理解 JS
引擎背后工作的原理悔橄,開發(fā)中幫助我們調(diào)試代碼靶累,同時也能應對面試中有關執(zhí)行棧的面試題。
執(zhí)行棧癣疟,在其它編程語言中被叫做“調(diào)用椪跫恚”,是一種 LIFO(后進先出)棧的數(shù)據(jù)結構睛挚,被用來存儲代碼運行時創(chuàng)建的所有執(zhí)行上下文邪蛔。
當 JS
引擎開始執(zhí)行第一行 JavaScript
代碼時,它會創(chuàng)建一個全局執(zhí)行上下文然后將它壓到執(zhí)行棧中扎狱,每當引擎遇到一個函數(shù)調(diào)用侧到,它會為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并壓入棧的頂部勃教。
引擎會執(zhí)行那些執(zhí)行上下文位于棧頂?shù)暮瘮?shù)。當該函數(shù)執(zhí)行結束時匠抗,執(zhí)行上下文從棧中彈出故源,控制流程到達當前棧中的下一個上下文。
結合下面代碼來理解:
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');
當上述代碼在瀏覽器加載時汞贸,JS
引擎創(chuàng)建了一個全局執(zhí)行上下文并把它壓入當前執(zhí)行棧绳军。當遇到 first()
JS
引擎為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并把它壓入當前執(zhí)行棧的頂部。
當從 first()
函數(shù)內(nèi)部調(diào)用 second()
JS
引擎為 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)了解了 JS
引擎是如何去管理執(zhí)行上下文的,那么蛔屹,執(zhí)行上下文是如何創(chuàng)建的呢削樊?
執(zhí)行上下文的創(chuàng)建分為兩個階段:
- 創(chuàng)建階段;
- 執(zhí)行階段兔毒;
創(chuàng)建階段
執(zhí)行上下文創(chuàng)建階段會做三件事:
- 綁定
this
- 創(chuàng)建詞法環(huán)境
- 創(chuàng)建變量環(huán)境
所以執(zhí)行上下文在概念上表示如下:
ExecutionContext = { // 執(zhí)行上下文
Binding This, // this值綁定
LexicalEnvironment = { ... }, // 詞法環(huán)境
VariableEnvironment = { ... }, // 變量環(huán)境
}
綁定 this
在全局執(zhí)行上下文中漫贞,this
的值指向全局對象。(在瀏覽器中育叁,this
引用 Window
對象)迅脐。
在函數(shù)執(zhí)行上下文中,this 的值取決于該函數(shù)是如何被調(diào)用的
- 通過對象方法調(diào)用函數(shù)豪嗽,
this
指向調(diào)用的對象 - 聲明函數(shù)后使用函數(shù)名稱普通調(diào)用谴蔑,
this
指向全局對象,嚴格模式下this
值是undefined
- 使用
new
方式調(diào)用函數(shù)龟梦,this
指向新創(chuàng)建的對象 - 使用
call
隐锭、apply
、bind
方式調(diào)用函數(shù)计贰,會改變this
的值钦睡,指向傳入的第一個參數(shù),例如
function fn () {
console.log(this)
}
function fn1 () {
'use strict'
console.log(this)
}
fn() // 普通函數(shù)調(diào)用躁倒,this 指向window對象
fn() // 嚴格模式下荞怒,this 值為 undefined
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 指向 'foo'
let bar = foo.baz;
bar(); // 'this' 指向全局 window 對象洒琢,因為沒有指定引用對象
let obj {
name: 'hello'
}
foo.baz.call(obj) // call 改變this值,指向obj對象
詞法環(huán)境
每一個詞法環(huán)境由下面兩部分組成:
- 環(huán)境記錄:變量對象 =》存儲聲明的變量和函數(shù)( let, const, function挣输,函數(shù)參數(shù))
- 外部環(huán)境引用:作用域鏈
ES6的官方文檔 把詞法環(huán)境定義為:
詞法環(huán)境(Lexical Environments)是一種規(guī)范類型纬凤,用于根據(jù)ECMAScript代碼的詞法嵌套結構來定義標識符與特定變量和函數(shù)的關聯(lián)。詞法環(huán)境由一個環(huán)境記錄(Environment Record)和一個可能為空的外部詞法環(huán)境(outer Lexical Environment)引用組成撩嚼。
簡單來說停士,詞法環(huán)境就是一種標識符—變量映射的結構(這里的標識符指的是變量/函數(shù)的名字,變量是對實際對象[包含函數(shù)和數(shù)組類型的對象]或基礎數(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>
}
環(huán)境記錄
所謂的環(huán)境記錄就是詞法環(huán)境中記錄變量和函數(shù)聲明的地方
環(huán)境記錄也有兩種類型:
聲明類環(huán)境記錄。顧名思義逻族,它存儲的是變量和函數(shù)聲明蜻底,函數(shù)的詞法環(huán)境內(nèi)部就包含著一個聲明類環(huán)境記錄。
對象環(huán)境記錄聘鳞。全局環(huán)境中的詞法環(huán)境中就包含的就是一個對象環(huán)境記錄薄辅。除了變量和函數(shù)聲明外,對象環(huán)境記錄還包括全局對象(瀏覽器的window
對象)抠璃。因此站楚,對于對象的每一個新增屬性(對瀏覽器來說,它包含瀏覽器提供給window對象的所有屬性和方法)搏嗡,都會在該記錄中創(chuàng)建一個新條目窿春。
注意:對函數(shù)而言,環(huán)境記錄還包含一個arguments對象采盒,該對象是個類數(shù)組對象旧乞,包含參數(shù)索引和參數(shù)的映射以及一個傳入函數(shù)的參數(shù)的長度屬性。舉個例子磅氨,一個arguments對象像下面這樣:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument 對象類似下面這樣
Arguments: { 0: 2, 1: 3, length: 2 }
環(huán)境記錄對象在創(chuàng)建階段也被稱為變量對象(VO)尺栖,在執(zhí)行階段被稱為活動對象(AO)。之所以被稱為變量對象是因為此時該對象只是存儲執(zhí)行上下文中變量和函數(shù)聲明烦租,之后代碼開始執(zhí)行决瞳,變量會逐漸被初始化或是修改,然后這個對象就被稱為活動對象
外部環(huán)境引用
對于外部環(huán)境的引用意味著在當前執(zhí)行上下文中可以訪問外部詞法環(huán)境左权。也就是說皮胡,如果在當前的詞法環(huán)境中找不到某個變量,那么Javascript
引擎會試圖在上層的詞法環(huán)境中尋找赏迟。(Javascript引擎會根據(jù)這個屬性來構成我們常說的作用域鏈)
詞法環(huán)境抽象出來類似下面的偽代碼:
GlobalExectionContext = { // 全局執(zhí)行上下文
this: <global object> // this 值綁定
LexicalEnvironment: { // 全局執(zhí)行上下文詞法環(huán)境
EnvironmentRecord: { // 環(huán)境記錄
Type: "Object",
// 標識符在這里綁定
}
outer: <null> // 外部引用
}
}
FunctionExectionContext = { // 函數(shù)執(zhí)行上下文
this: <depends on how function is called> // this 值綁定
LexicalEnvironment: { // 函數(shù)執(zhí)行上下文詞法環(huán)境
EnvironmentRecord: { // 環(huán)境記錄
Type: "Declarative",
// 標識符在這里綁定
}
outer: <Global or outer function environment reference> // 引用全局環(huán)境
}
}
變量環(huán)境
它同樣是一個詞法環(huán)境屡贺,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關系。
如上所述,變量環(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
定義的變量并沒有關聯(lián)任何值究抓,但 var
定義的變量被設成了 undefined
。
這是因為在創(chuàng)建階段時袭灯,引擎檢查代碼找出變量和函數(shù)聲明刺下,雖然函數(shù)聲明完全存儲在環(huán)境中,但是變量最初設置為 undefined
(var
情況下)稽荧,或者未初始化(let
和 const
情況下)橘茉。
這就是為什么你可以在聲明之前訪問 var
定義的變量(雖然是 undefined
),但是在聲明之前訪問 let
和 const
的變量會得到一個引用錯誤姨丈。
這就是我們說的變量聲明提升畅卓。
執(zhí)行階段
經(jīng)過上面的創(chuàng)建執(zhí)行上下文,就開始執(zhí)行 JavaScript
代碼了蟋恬。在執(zhí)行階段翁潘,如果 JavaScript
引擎不能在源碼中聲明的實際位置找到 let
變量的值,它會被賦值為 undefined
筋现。
執(zhí)行棧應用
利用瀏覽器查看棧的調(diào)用信息
我們知道執(zhí)行棧是用來管理執(zhí)行上下文調(diào)用關系的數(shù)據(jù)結構,那么我們在實際工作中如何運用它呢箱歧。
答案是我們可以借助瀏覽器“開發(fā)者工具” source
標簽矾飞,選擇 JavaScript
代碼打上斷點,就可以查看函數(shù)的調(diào)用關系呀邢,并且可以切換查看每個函數(shù)的變量值
我們在 second
函數(shù)內(nèi)部打上斷點洒沦,就可以看到右邊 Call Stack
調(diào)用棧顯示 second
、first
价淌、(anonymous)
調(diào)用關系申眼,second
是在棧頂(anonymous
在棧底相當于全局執(zhí)行上下文),執(zhí)行second
函數(shù)我們可以查看該函數(shù)作用域 Scope
局部變量a
蝉衣、b
和 num
的值括尸,通過查看調(diào)用棧的調(diào)用關系我們可以快速定位到我們代碼執(zhí)行的情況。
那如果代碼執(zhí)行出錯病毡,也不知道在哪個地方打斷點調(diào)試濒翻,那怎么查看出錯地方的調(diào)用棧呢,告訴大家一個技巧,如下圖
我們不用打斷點有送,執(zhí)行上面兩步操作淌喻,就可以在代碼執(zhí)行異常的地方自動打上斷點。知道這個技巧后雀摘,再也不用擔心代碼出錯了裸删。
除了上面通過斷點來查看調(diào)用棧,還可以使用 console.trace() 來輸出當前的函數(shù)調(diào)用關系阵赠,比如在示例代碼中的 second
函數(shù)里面加上了 console.trace()涯塔,就可以看到控制臺輸出的結果,如下圖:
總結
JavaScript
執(zhí)行分為兩個階段豌注,編譯階段和執(zhí)行階段伤塌。編譯階段會經(jīng)過詞法分析、語法分析轧铁、代碼生成步驟生成可執(zhí)行代碼每聪; JS
引擎執(zhí)行可執(zhí)行性代碼會創(chuàng)建執(zhí)行上下文,包括綁定this
齿风、創(chuàng)建詞法環(huán)境和變量環(huán)境药薯;詞法環(huán)境創(chuàng)建外部引用(作用域鏈)和 記錄環(huán)境(變量對象,let, const, function, arguments)救斑, JS
引擎創(chuàng)建執(zhí)行上下完成后開始單線程從上到下一行一行執(zhí)行 JS
代碼了童本。
最后,分享了在開發(fā)過程中一些調(diào)用棧的的應用技巧脸候。
引用鏈接
JavaScript 語法解析穷娱、AST毕荐、V8砂缩、JIT
[譯] 理解 JavaScript 中的執(zhí)行上下文和執(zhí)行棧
理解Javascript中的執(zhí)行上下文和執(zhí)行棧
推薦閱讀