深入了解JavaScript執(zhí)行過程(JS系列之一)

前言

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)站地址查看 tokenhttps://esprima.org/demo/parse.html

1詞法分析1.png
1詞法分析2.png

語法分析

語法分析階段會將詞法單元流(數(shù)組)衅金,也就是上面所說的token, 轉(zhuǎn)換成樹狀結構的 “抽象語法樹(AST)”

2語法分析.png

代碼生成

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');
stack.png

當上述代碼在瀏覽器加載時汞贸,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隐锭、applybind 方式調(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)注意到 letconst 定義的變量并沒有關聯(lián)任何值究抓,但 var 定義的變量被設成了 undefined

這是因為在創(chuàng)建階段時袭灯,引擎檢查代碼找出變量和函數(shù)聲明刺下,雖然函數(shù)聲明完全存儲在環(huán)境中,但是變量最初設置為 undefinedvar 情況下)稽荧,或者未初始化(letconst 情況下)橘茉。

這就是為什么你可以在聲明之前訪問 var 定義的變量(雖然是 undefined),但是在聲明之前訪問 letconst 的變量會得到一個引用錯誤姨丈。

這就是我們說的變量聲明提升畅卓。

執(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ù)的變量值

調(diào)用棧.png

我們在 second 函數(shù)內(nèi)部打上斷點洒沦,就可以看到右邊 Call Stack 調(diào)用棧顯示 secondfirst价淌、(anonymous) 調(diào)用關系申眼,second 是在棧頂(anonymous 在棧底相當于全局執(zhí)行上下文),執(zhí)行second函數(shù)我們可以查看該函數(shù)作用域 Scope 局部變量a蝉衣、bnum的值括尸,通過查看調(diào)用棧的調(diào)用關系我們可以快速定位到我們代碼執(zhí)行的情況。

那如果代碼執(zhí)行出錯病毡,也不知道在哪個地方打斷點調(diào)試濒翻,那怎么查看出錯地方的調(diào)用棧呢,告訴大家一個技巧,如下圖

調(diào)用棧2.png

我們不用打斷點有送,執(zhí)行上面兩步操作淌喻,就可以在代碼執(zhí)行異常的地方自動打上斷點。知道這個技巧后雀摘,再也不用擔心代碼出錯了裸删。

除了上面通過斷點來查看調(diào)用棧,還可以使用 console.trace() 來輸出當前的函數(shù)調(diào)用關系阵赠,比如在示例代碼中的 second 函數(shù)里面加上了 console.trace()涯塔,就可以看到控制臺輸出的結果,如下圖:

調(diào)用棧3.png

總結

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í)行棧

推薦閱讀

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谆趾,隨后出現(xiàn)的幾起案子携添,更是在濱河造成了極大的恐慌嫁盲,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈掠,死亡現(xiàn)場離奇詭異羞秤,居然都是意外死亡,警方通過查閱死者的電腦和手機左敌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門瘾蛋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人矫限,你說我怎么就攤上這事瘦黑【└铮” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵幸斥,是天一觀的道長匹摇。 經(jīng)常有香客問我,道長甲葬,這世上最難降的妖魔是什么廊勃? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮经窖,結果婚禮上坡垫,老公的妹妹穿的比我還像新娘。我一直安慰自己画侣,他們只是感情好冰悠,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著配乱,像睡著了一般溉卓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搬泥,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天桑寨,我揣著相機與錄音,去河邊找鬼忿檩。 笑死尉尾,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的燥透。 我是一名探鬼主播沙咏,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼班套!你這毒婦竟也來了肢藐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤孽尽,失蹤者是張志新(化名)和其女友劉穎窖壕,沒想到半個月后忧勿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杉女,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年鸳吸,在試婚紗的時候發(fā)現(xiàn)自己被綠了熏挎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡晌砾,死狀恐怖坎拐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤哼勇,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布都伪,位于F島的核電站,受9級特大地震影響积担,放射性物質(zhì)發(fā)生泄漏陨晶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一帝璧、第九天 我趴在偏房一處隱蔽的房頂上張望先誉。 院中可真熱鬧,春花似錦的烁、人聲如沸褐耳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铃芦。三九已至,卻和暖如春把曼,著一層夾襖步出監(jiān)牢的瞬間杨帽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工嗤军, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留注盈,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓叙赚,卻偏偏與公主長得像老客,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子震叮,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容