手把手教會你JavaScript引擎如何執(zhí)行JavaScript代碼

JavaScript 在運行過程中與其他語言有所不一樣柒凉,如果不理解 JavaScript 的詞法環(huán)境、執(zhí)行上下文等內(nèi)容篓跛,很容易會在開發(fā)過程中產(chǎn)生 Bug膝捞,比如this指向和預(yù)期不一致、某個變量不知道為什么被改了愧沟,等等蔬咬。所以今天我們就來聊一聊 JavaScript 代碼的運行過程。

大家都知道沐寺,JavaScript 代碼是需要在 JavaScript 引擎中運行的林艘。我們在說到 JavaScript 運行的時候,常常會提到執(zhí)行環(huán)境芽丹、詞法環(huán)境北启、作用域、執(zhí)行上下文拔第、閉包等內(nèi)容咕村。這些概念看起來都差不多,卻好像又不大容易區(qū)分清楚蚊俺,它們分別都在描述什么呢懈涛?

這些詞語都是與 JavaScript 引擎執(zhí)行代碼的過程有關(guān),為了搞清楚這些概念之間的區(qū)別泳猬,我們可以回顧下 JavaScript 代碼運行過程中的各個階段批钠。

JavaScript 代碼運行的各個階段

JavaScript 是弱類型語言,在運行時才能確定變量類型得封。JavaScript 引擎在執(zhí)行 JavaScript 代碼時埋心,也會從上到下進行詞法分析、語法分析忙上、語義分析等處理拷呆,并在代碼解析完成后生成 AST(抽象語法樹),最終根據(jù) AST 生成 CPU 可以執(zhí)行的機器碼并執(zhí)行疫粥。

這個過程茬斧,我們稱之為語法分析階段。除了語法分析階段梗逮,JavaScript 引擎在執(zhí)行代碼時還會進行其他的處理项秉。以 V8 引擎為例,在 V8 引擎中 JavaScript 代碼的運行過程主要分成三個階段慷彤。

  1. 語法分析階段娄蔼。該階段會對代碼進行語法分析怖喻,檢查是否有語法錯誤(SyntaxError),如果發(fā)現(xiàn)語法錯誤岁诉,會在控制臺拋出異常并終止執(zhí)行罢防。

  2. 編譯階段。該階段會進行執(zhí)行上下文(Execution Context)的創(chuàng)建唉侄,包括創(chuàng)建變量對象、建立作用域鏈野建、確定 this 的指向等属划。每進入一個不同的運行環(huán)境時,V8 引擎都會創(chuàng)建一個新的執(zhí)行上下文候生。

  3. 執(zhí)行階段同眯。將編譯階段中創(chuàng)建的執(zhí)行上下文壓入調(diào)用棧,并成為正在運行的執(zhí)行上下文唯鸭,代碼執(zhí)行結(jié)束后须蜗,將其彈出調(diào)用棧。

其中目溉,語法分析階段屬于編譯器通用內(nèi)容明肮,就不再贅述。前面提到的執(zhí)行環(huán)境缭付、詞法環(huán)境柿估、作用域、執(zhí)行上下文等內(nèi)容都是在編譯和執(zhí)行階段中產(chǎn)生的概念陷猫。

執(zhí)行上下文的創(chuàng)建

執(zhí)行上下文的創(chuàng)建離不開 JavaScript 的運行環(huán)境秫舌,JavaScript 運行環(huán)境包括全局環(huán)境、函數(shù)環(huán)境和eval绣檬,其中全局環(huán)境和函數(shù)環(huán)境的創(chuàng)建過程如下:

  1. 第一次載入 JavaScript 代碼時足陨,首先會創(chuàng)建一個全局環(huán)境。全局環(huán)境位于最外層娇未,直到應(yīng)用程序退出后(例如關(guān)閉瀏覽器和網(wǎng)頁)才會被銷毀墨缘。
  2. 每個函數(shù)都有自己的運行環(huán)境,當函數(shù)被調(diào)用時忘蟹,則會進入該函數(shù)的運行環(huán)境飒房。當該環(huán)境中的代碼被全部執(zhí)行完畢后,該環(huán)境會被銷毀媚值。不同的函數(shù)運行環(huán)境不一樣狠毯,即使是同一個函數(shù),在被多次調(diào)用時也會創(chuàng)建多個不同的函數(shù)環(huán)境褥芒。

在不同的運行環(huán)境中嚼松,變量和函數(shù)可訪問的其他數(shù)據(jù)范圍不同嫡良,環(huán)境的行為(比如創(chuàng)建和銷毀)也有所區(qū)別。而每進入一個不同的運行環(huán)境時献酗,JavaScript 都會創(chuàng)建一個新的執(zhí)行上下文寝受,該過程包括:

  • 建立作用域鏈(Scope Chain);
  • 創(chuàng)建變量對象(Variable Object罕偎,簡稱 VO)很澄;
  • 確定 this 的指向。

由于建立作用域鏈過程中會涉及變量對象的概念颜及,因此我們先來看看變量對象的創(chuàng)建甩苛,再看建立作用域鏈和確定 this 的指向。

創(chuàng)建變量對象

變量對象(VO)

每個執(zhí)行上下文都會有一個關(guān)聯(lián)的變量對象俏站,該對象上會保存這個上下文中定義的所有變量和函數(shù)讯蒲。

在瀏覽器中,全局環(huán)境的變量對象是window對象肄扎,因此所有的全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的墨林。相應(yīng)的,在 Node 中全局環(huán)境的變量對象則是global對象犯祠。

創(chuàng)建VO的過程

創(chuàng)建變量對象將會創(chuàng)建arguments對象(僅函數(shù)環(huán)境下)旭等,同時會檢查當前上下文的函數(shù)聲明和變量聲明。

  • 對于變量聲明:此時會給變量分配內(nèi)存雷则,并將其初始化為undefined(該過程只進行定義聲明辆雾,執(zhí)行階段才執(zhí)行賦值語句)。
  • 對于函數(shù)聲明:此時會在內(nèi)存里創(chuàng)建函數(shù)對象月劈,并且直接初始化為該函數(shù)對象度迂。

變量聲明和函數(shù)聲明的處理過程,便是我們常說的變量提升和函數(shù)提升猜揪,其中函數(shù)聲明提升會優(yōu)先于變量聲明提升惭墓。因為變量提升容易帶來變量在預(yù)期外被覆蓋掉的問題,同時還可能導(dǎo)致本應(yīng)該被銷毀的變量沒有被銷毀等情況而姐。因此 ES6 中引入了let和const關(guān)鍵字腊凶,從而使 JavaScript 也擁有了塊級作用域。

作用域

在各類編程語言中拴念,作用域分為靜態(tài)作用域和動態(tài)作用域钧萍。JavaScript 采用的是詞法作用域(Lexical Scoping),也就是靜態(tài)作用域政鼠。詞法作用域中的變量风瘦,在編譯過程中會產(chǎn)生一個確定的作用域。

詞法作用域中的變量公般,在編譯過程中會產(chǎn)生一個確定的作用域万搔,這個作用域即當前的執(zhí)行上下文胡桨,在 ES5 后我們使用詞法環(huán)境(Lexical Environment)替代作用域來描述該執(zhí)行上下文。因此瞬雹,詞法環(huán)境可理解為我們常說的作用域昧谊,同樣也指當前的執(zhí)行上下文(注意,是當前的執(zhí)行上下文)酗捌。

在 JavaScript 中呢诬,詞法環(huán)境又分為詞法環(huán)境(Lexical Environment)和變量環(huán)境(Variable Environment)兩種,其中:

  • 變量環(huán)境用來記錄var/function等變量聲明胖缤;
  • 詞法環(huán)境是用來記錄let/const/class等變量聲明馅巷。

也就是說,創(chuàng)建變量過程中會進行函數(shù)提升和變量提升草姻,JavaScript 會通過詞法環(huán)境來記錄函數(shù)和變量聲明。通過使用兩個詞法環(huán)境(而不是一個)分別記錄不同的變量聲明內(nèi)容稍刀,JavaScript 實現(xiàn)了支持塊級作用域的同時撩独,不影響原有的變量聲明和函數(shù)聲明。

這就是創(chuàng)建變量的過程账月,它屬于執(zhí)行上下文創(chuàng)建中的一環(huán)综膀。創(chuàng)建變量的過程會產(chǎn)生作用域,作用域也被稱為詞法環(huán)境局齿。

建立作用域鏈

作用域鏈剧劝,就是將各個作用域通過某種方式連接在一起。作用域就是詞法環(huán)境抓歼,而詞法環(huán)境由兩個成員組成讥此。

  1. 環(huán)境記錄(Environment Record):用于記錄自身詞法環(huán)境中的變量對象。
  2. 外部詞法環(huán)境引用(Outer Lexical Environment):記錄外層詞法環(huán)境的引用谣妻。

通過外部詞法環(huán)境的引用萄喳,作用域可以層層拓展,建立起從里到外延伸的一條作用域鏈蹋半。當某個變量無法在自身詞法環(huán)境記錄中找到時他巨,可以根據(jù)外部詞法環(huán)境引用向外層進行尋找,直到最外層的詞法環(huán)境中外部詞法環(huán)境引用為null减江,這便是作用域鏈的變量查詢染突。

JavaScript 代碼運行過程分為定義期和執(zhí)行期,前面提到的編譯階段則屬于定義期辈灼,代碼示例如下:

function foo() { // 定義全局函數(shù)foo
    console.dir(bar);
    var a = 1;
    function bar() { // 在foo函數(shù)內(nèi)部定義函數(shù)bar
        a = 2;
 }
}
console.dir(foo);
foo();

前面我們說到份企,JavaScript 使用的是靜態(tài)作用域,因此函數(shù)的作用域在定義期已經(jīng)決定了茵休。在上面的例子中薪棒,全局函數(shù)foo創(chuàng)建了一個foo[[scope]]屬性手蝎,包含了全局[[scope]]

foo[[scope]] = [globalContext];

而當我們執(zhí)行foo()時,也會分別進入foo函數(shù)的定義期和執(zhí)行期俐芯。

在foo函數(shù)的定義期時棵介,函數(shù)bar的[[scope]]將會包含全局[[scope]]和foo的[[scope]]:

bar[[scope]] = [fooContext, globalContext];

運行上述代碼,我們可以在控制臺看到符合預(yù)期的輸出:

可以看到:

  • foo的[[scope]]屬性包含了全局[[scope]]
  • bar的[[scope]]將會包含全局[[scope]]和foo的[[scope]]

也就是說吧史,JavaScript 會通過外部詞法環(huán)境引用來創(chuàng)建變量對象的一個作用域鏈邮辽,從而保證對執(zhí)行環(huán)境有權(quán)訪問的變量和函數(shù)的有序訪問。除了創(chuàng)建作用域鏈之外贸营,在這個過程中還會對創(chuàng)建的變量對象做一些處理吨述。

在編譯階段會進行變量對象(VO)的創(chuàng)建,該過程會進行函數(shù)聲明和變量聲明钞脂,這時候變量的值被初始化為 undefined揣云。在代碼進入執(zhí)行階段之后,JavaScript 會對變量進行賦值冰啃,此時變量對象會轉(zhuǎn)為活動對象(Active Object邓夕,簡稱 AO),轉(zhuǎn)換后的活動對象才可被訪問阎毅,這就是 VO -> AO 的過程焚刚,示例如下:

function foo(a) {
    var b = 2; 
    function c() {}
    var d = function() {};
}

foo(1);

在執(zhí)行foo(1)時,首先進入定義期扇调,此時:

  • 參數(shù)變量a的值為1
  • 變量b和d初始化為undefined
  • 函數(shù)c創(chuàng)建函數(shù)并初始化
AO = {
 arguments: {
  0: 1,
  length: 1
 },
 a: 1,
 b: undefined,
 c: reference to function() c() {}
 d:undefined
}

前面我們也有提到矿咕,進入執(zhí)行期之后,會執(zhí)行賦值語句進行賦值狼钮,此時變量b和d會被賦值為 2 和函數(shù)表達式:

AO = {
   arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  b: 2,
  c: reference to function c(){},
  d: reference to FunctionExpression "d"
}

這就是 VO -> AO 過程碳柱。

  • 在定義期(編譯階段):該對象值仍為undefined,且處于不可訪問的狀態(tài)熬芜。
  • 進入執(zhí)行期(執(zhí)行階段):VO 被激活士聪,其中變量屬性會進行賦值。

實際上在執(zhí)行的時候猛蔽,除了 VO 被激活剥悟,活動對象還會添加函數(shù)執(zhí)行時傳入的參數(shù)和arguments這個特殊對象,因此 AO 和 VO 的關(guān)系可以用以下關(guān)系來表達:

AO = VO + function parameters + arguments

現(xiàn)在曼库,我們知道作用域鏈是在進入代碼的執(zhí)行階段時区岗,通過外部詞法環(huán)境引用來創(chuàng)建的』倏荩總結(jié)如下:

  • 在編譯階段慈缔,JavaScript 在創(chuàng)建執(zhí)行上下文的時候會先創(chuàng)建變量對象(VO);
  • 在執(zhí)行階段种玛,變量對象(VO)被激活為活動對象( AO)藐鹤,函數(shù)內(nèi)部的變量對象通過外部詞法環(huán)境的引用創(chuàng)建作用域鏈瓤檐。

通過作用域鏈,我們可以在函數(shù)內(nèi)部可以直接讀取外部以及全局變量娱节,但外部環(huán)境是無法訪問內(nèi)部函數(shù)里的變量挠蛉。示例如下:

function foo() {
  var a = 1;
}
foo();
console.log(a); // undefined

我們在全局環(huán)境下無法訪問函數(shù)foo中的變量a,這是因為全局函數(shù)的作用域鏈里肄满,不含有函數(shù)foo內(nèi)的作用域谴古。

如果我們想要訪問內(nèi)部函數(shù)的變量,可以通過函數(shù)foo中的函數(shù)bar返回變量a稠歉,并將函數(shù)bar返回掰担,這樣我們在全局環(huán)境中也可以通過調(diào)用函數(shù)foo返回的函數(shù)bar,來訪問變量a:

function foo() {
  var a = 1;
  function bar() {
    return a;
  }
  return bar;
}
var b = foo();
console.log(b()); // 1

當函數(shù)執(zhí)行結(jié)束之后怒炸,執(zhí)行期上下文將被銷毀带饱,其中包括作用域鏈和激活對象烘贴。

在上面的實例中轧叽;當b()執(zhí)行時,foo函數(shù)上下文包括作用域都已經(jīng)被銷毀了杨幼,但是foo作用域下的a依然可以被訪問到灯蝴;這是因為bar函數(shù)引用了foo函數(shù)變量對象中的值,此時即使創(chuàng)建bar函數(shù)的foo函數(shù)執(zhí)行上下文被銷毀了孝宗,但它的變量對象依然會保留在 JavaScript 內(nèi)存中穷躁,bar函數(shù)依然可以通過bar函數(shù)的作用域鏈找到它,并進行訪問因妇。這就是閉包问潭;

閉包使得我們可以從外部讀取局部變量,常見的用途包括:

  1. 用于從外部讀取其他函數(shù)內(nèi)部變量的函數(shù)婚被;
  2. 可以使用閉包來模擬私有方法狡忙;
  3. 讓這些變量的值始終保持在內(nèi)存中。

注意址芯,在使用閉包的時候灾茁,需要及時清理不再使用到的變量,否則可能導(dǎo)致內(nèi)存泄漏問題谷炸。

確定 this 的指向

在 JavaScript 中北专,this指向執(zhí)行當前代碼對象的所有者,可簡單理解為this指向最后調(diào)用當前代碼的那個對象旬陡。

根據(jù) JavaScript 中函數(shù)的調(diào)用方式不同拓颓,this的指向分為以下情況。

  1. 在全局環(huán)境中描孟,this指向全局對象(在瀏覽器中為window)
  2. 在函數(shù)內(nèi)部驶睦,this的值取決于函數(shù)被調(diào)用的方式
  3. 函數(shù)作為對象的方法被調(diào)用砰左,this指向調(diào)用這個方法的對象
  4. 函數(shù)用作構(gòu)造函數(shù)時(使用new關(guān)鍵字),它的this被綁定到正在構(gòu)造的新對象
  5. 在類的構(gòu)造函數(shù)中场航,this是一個常規(guī)對象缠导,類中所有非靜態(tài)的方法都會被添加到this的原型中
  6. 在箭頭函數(shù)中,this指向它被創(chuàng)建時的環(huán)境
  7. 使用apply旗闽、call酬核、bind等方式調(diào)用:根據(jù) API 不同,可切換函數(shù)執(zhí)行的上下文環(huán)境适室,即this綁定的對象

可以看到嫡意,this在不同的情況下會有不同的指向,在 ES6 箭頭函數(shù)還沒出現(xiàn)之前捣辆,為了能正確獲取某個運行環(huán)境下this對象蔬螟,我們常常會使用以下代碼:

var that = this;
var self = this;

這樣的代碼將變量分配給this,便于使用汽畴。但是降低了代碼可讀性旧巾,不推薦使用,通過正確使用箭頭函數(shù)忍些,我們可以更好地管理作用域鲁猩。

總結(jié)

今天我們了解了 JavaScript 代碼的運行過程,該過程分為語法分析階段罢坝、編譯階段廓握、執(zhí)行階段三個階段。

在編譯階段嘁酿,JavaScript會進行執(zhí)行上下文的創(chuàng)建隙券,在執(zhí)行階段,變量對象(VO)會被激活為活動對象(AO)闹司,變量會進行賦值娱仔,此時活動對象才可被訪問。在執(zhí)行結(jié)束之后游桩,作用域鏈和活動對象均被銷毀牲迫,使用閉包可使活動對象依然被保留在內(nèi)存中。這就是 JavaScript 代碼的運行過程借卧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恩溅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谓娃,更是在濱河造成了極大的恐慌脚乡,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異奶稠,居然都是意外死亡俯艰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門锌订,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竹握,“玉大人,你說我怎么就攤上這事辆飘±卜” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵蜈项,是天一觀的道長芹关。 經(jīng)常有香客問我,道長紧卒,這世上最難降的妖魔是什么侥衬? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮跑芳,結(jié)果婚禮上轴总,老公的妹妹穿的比我還像新娘。我一直安慰自己博个,他們只是感情好怀樟,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盆佣,像睡著了一般往堡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上罪塔,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音养葵,去河邊找鬼征堪。 笑死,一個胖子當著我的面吹牛关拒,可吹牛的內(nèi)容都是我干的佃蚜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼着绊,長吁一口氣:“原來是場噩夢啊……” “哼谐算!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起归露,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤洲脂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后剧包,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恐锦,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡往果,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了一铅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陕贮。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖潘飘,靈堂內(nèi)的尸體忽然破棺而出肮之,到底是詐尸還是另有隱情,我是刑警寧澤卜录,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布戈擒,位于F島的核電站,受9級特大地震影響暴凑,放射性物質(zhì)發(fā)生泄漏峦甩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一现喳、第九天 我趴在偏房一處隱蔽的房頂上張望凯傲。 院中可真熱鬧,春花似錦嗦篱、人聲如沸冰单。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诫欠。三九已至,卻和暖如春浴栽,著一層夾襖步出監(jiān)牢的瞬間荒叼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工典鸡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留被廓,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓萝玷,卻偏偏與公主長得像嫁乘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子球碉,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

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