瀏覽器首先按順序加載由<script>標(biāo)簽分割的js代碼塊僵驰,加載js代碼塊完畢后想际,立刻進(jìn)入以下三個(gè)階段咽瓷,然后再按順序查找下一個(gè)代碼塊崔挖,再繼續(xù)執(zhí)行以下三個(gè)階段贸街,無(wú)論是外部腳本文件(不異步加載)還是內(nèi)部腳本代碼塊,都是一樣的原理狸相,并且都在同一個(gè)全局作用域中薛匪。
JS引擎線程的執(zhí)行過(guò)程的三個(gè)階段:
- 語(yǔ)法分析
- 預(yù)編譯階段
- 執(zhí)行階段
一. 語(yǔ)法分析
分析該js腳本代碼塊的語(yǔ)法是否正確,如果出現(xiàn)不正確脓鹃,則向外拋出一個(gè)語(yǔ)法錯(cuò)誤(SyntaxError)逸尖,停止該js代碼塊的執(zhí)行,然后繼續(xù)查找并加載下一個(gè)代碼塊瘸右;如果語(yǔ)法正確娇跟,則進(jìn)入預(yù)編譯階段。
下面階段的代碼執(zhí)行不會(huì)再進(jìn)行語(yǔ)法校驗(yàn)太颤,語(yǔ)法分析在代碼塊加載完畢時(shí)統(tǒng)一檢驗(yàn)語(yǔ)法苞俘。
二. 預(yù)編譯階段
1. js的運(yùn)行環(huán)境
全局環(huán)境(JS代碼加載完畢后,進(jìn)入代碼預(yù)編譯即進(jìn)入全局環(huán)境)
函數(shù)環(huán)境(函數(shù)調(diào)用執(zhí)行時(shí)龄章,進(jìn)入該函數(shù)環(huán)境吃谣,不同的函數(shù)則函數(shù)環(huán)境不同)
eval(不建議使用乞封,會(huì)有安全,性能等問(wèn)題)
每進(jìn)入一個(gè)不同的運(yùn)行環(huán)境都會(huì)創(chuàng)建一個(gè)相應(yīng)的執(zhí)行上下文(Execution Context)岗憋,那么在一段JS程序中一般都會(huì)創(chuàng)建多個(gè)執(zhí)行上下文肃晚,js引擎會(huì)以棧的方式對(duì)這些執(zhí)行上下文進(jìn)行處理,形成函數(shù)調(diào)用棧(call stack)仔戈,棧底永遠(yuǎn)是全局執(zhí)行上下文(Global Execution Context)关串,棧頂則永遠(yuǎn)是當(dāng)前執(zhí)行上下文。
2. 函數(shù)調(diào)用棧/執(zhí)行棧
調(diào)用棧监徘,也叫執(zhí)行棧晋修,具有LIFO(后進(jìn)先出)結(jié)構(gòu),用于存儲(chǔ)在代碼執(zhí)行期間創(chuàng)建的所有執(zhí)行上下文耐量。
首次運(yùn)行JS代碼時(shí)飞蚓,會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并Push到當(dāng)前的執(zhí)行棧中。每當(dāng)發(fā)生函數(shù)調(diào)用廊蜒,引擎都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文并Push到當(dāng)前執(zhí)行棧的棧頂趴拧。
當(dāng)棧頂函數(shù)運(yùn)行完成后,其對(duì)應(yīng)的函數(shù)執(zhí)行上下文將會(huì)從執(zhí)行棧中Pop出山叮,上下文控制權(quán)將移到當(dāng)前執(zhí)行棧的下一個(gè)執(zhí)行上下文著榴。
var 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');
// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
[圖片上傳失敗...(image-bd202f-1551101247545)]
3. 執(zhí)行上下文的創(chuàng)建
執(zhí)行上下文可理解為當(dāng)前的執(zhí)行環(huán)境,與該運(yùn)行環(huán)境相對(duì)應(yīng)屁倔,具體分類如上面所說(shuō)分為全局執(zhí)行上下文和函數(shù)執(zhí)行上下文脑又。創(chuàng)建執(zhí)行上下文的三部曲:
創(chuàng)建變量對(duì)象(Variable Object)
建立作用域鏈(Scope Chain)
確定this的指向
3.1 創(chuàng)建變量對(duì)象
創(chuàng)建arguments對(duì)象:檢查當(dāng)前上下文中的參數(shù),建立該對(duì)象的屬性與屬性值锐借,僅在函數(shù)環(huán)境(非箭頭函數(shù))中進(jìn)行问麸,全局環(huán)境沒(méi)有此過(guò)程
檢查當(dāng)前上下文的函數(shù)聲明:按代碼順序查找,將找到的函數(shù)提前聲明钞翔,如果當(dāng)前上下文的變量對(duì)象沒(méi)有該函數(shù)名屬性严卖,則在該變量對(duì)象以函數(shù)名建立一個(gè)屬性,屬性值則為指向該函數(shù)所在堆內(nèi)存地址的引用布轿,如果存在哮笆,則會(huì)被新的引用覆蓋。
檢查當(dāng)前上下文的變量聲明:按代碼順序查找汰扭,將找到的變量提前聲明稠肘,如果當(dāng)前上下文的變量對(duì)象沒(méi)有該變量名屬性,則在該變量對(duì)象以變量名建立一個(gè)屬性萝毛,屬性值為undefined项阴;如果存在,則忽略該變量聲明
函數(shù)聲明提前和變量聲明提升是在創(chuàng)建變量對(duì)象中進(jìn)行的笆包,且函數(shù)聲明優(yōu)先級(jí)高于變量聲明环揽。具體是如何函數(shù)和變量聲明提前的可以看后面拷沸。
創(chuàng)建變量對(duì)象發(fā)生在預(yù)編譯階段,但尚未進(jìn)入執(zhí)行階段薯演,該變量對(duì)象都是不能訪問(wèn)的,因?yàn)榇藭r(shí)的變量對(duì)象中的變量屬性尚未賦值秧了,值仍為undefined跨扮,只有進(jìn)入執(zhí)行階段,變量對(duì)象中的變量屬性進(jìn)行賦值后验毡,變量對(duì)象(Variable Object)轉(zhuǎn)為活動(dòng)對(duì)象(Active Object)后衡创,才能進(jìn)行訪問(wèn),這個(gè)過(guò)程就是VO –> AO過(guò)程晶通。
3.2 建立作用域鏈
通俗理解璃氢,作用域鏈由當(dāng)前執(zhí)行環(huán)境的變量對(duì)象(未進(jìn)入執(zhí)行階段前)與上層環(huán)境的一系列活動(dòng)對(duì)象組成,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問(wèn)權(quán)限的變量和函數(shù)的有序訪問(wèn)狮辽。
可以通過(guò)一個(gè)例子簡(jiǎn)單理解:
var num = 30;
function test() {
var a = 10;
function innerTest() {
var b = 20;
return a + b
}
innerTest()
}
test()
在上面的例子中一也,當(dāng)執(zhí)行到調(diào)用innerTest函數(shù),進(jìn)入innerTest函數(shù)環(huán)境喉脖。全局執(zhí)行上下文和test函數(shù)執(zhí)行上下文已進(jìn)入執(zhí)行階段椰苟,innerTest函數(shù)執(zhí)行上下文在預(yù)編譯階段創(chuàng)建變量對(duì)象,所以他們的活動(dòng)對(duì)象和變量對(duì)象分別是AO(global)树叽,AO(test)和VO(innerTest)舆蝴,而innerTest的作用域鏈由當(dāng)前執(zhí)行環(huán)境的變量對(duì)象(未進(jìn)入執(zhí)行階段前)與上層環(huán)境的一系列活動(dòng)對(duì)象組成,如下:
innerTestEC = {
//變量對(duì)象
VO: {b: undefined},
//作用域鏈
scopeChain: [VO(innerTest), AO(test), AO(global)],
//this指向
this: window
}
深入理解的話题诵,創(chuàng)建作用域鏈洁仗,也就是創(chuàng)建詞法環(huán)境,而詞法環(huán)境有兩個(gè)組成部分:
- 環(huán)境記錄:存儲(chǔ)變量和函數(shù)聲明的實(shí)際位置
- 對(duì)外部環(huán)境的引用:可以訪問(wèn)其外部詞法環(huán)境
詞法環(huán)境類型偽代碼如下:
// 第一種類型: 全局環(huán)境
GlobalExectionContext = { // 全局執(zhí)行上下文
LexicalEnvironment: { // 詞法環(huán)境
EnvironmentRecord: { // 環(huán)境記錄
Type: "Object", // 全局環(huán)境
// 標(biāo)識(shí)符綁定在這里
outer: <null> // 對(duì)外部環(huán)境的引用
}
}
// 第二種類型: 函數(shù)環(huán)境
FunctionExectionContext = { // 函數(shù)執(zhí)行上下文
LexicalEnvironment: { // 詞法環(huán)境
EnvironmentRecord: { // 環(huán)境記錄
Type: "Declarative", // 函數(shù)環(huán)境
// 標(biāo)識(shí)符綁定在這里 // 對(duì)外部環(huán)境的引用
outer: <Global or outer function environment reference>
}
}
在創(chuàng)建變量對(duì)象性锭,也就是創(chuàng)建變量環(huán)境赠潦,而變量環(huán)境也是一個(gè)詞法環(huán)境。在 ES6 中篷店,詞法 環(huán)境和 變量 環(huán)境的區(qū)別在于前者用于存儲(chǔ)函數(shù)聲明和變量( let
和 const
)綁定祭椰,而后者僅用于存儲(chǔ)變量( 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",
// 標(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>
}
}
變量提升的具體原因:在創(chuàng)建階段疲陕,函數(shù)聲明存儲(chǔ)在環(huán)境中方淤,而變量會(huì)被設(shè)置為 undefined
(在 var
的情況下)或保持未初始化(在 let
和 const
的情況下)。所以這就是為什么可以在聲明之前訪問(wèn) var
定義的變量(盡管是 undefined
)蹄殃,但如果在聲明之前訪問(wèn) let
和 const
定義的變量就會(huì)提示引用錯(cuò)誤的原因携茂。此時(shí)let 和 const處于未初始化狀態(tài)不能使用,只有進(jìn)入執(zhí)行階段诅岩,變量對(duì)象中的變量屬性進(jìn)行賦值后讳苦,變量對(duì)象(Variable Object)轉(zhuǎn)為活動(dòng)對(duì)象(Active Object)后带膜,let
和const
才能進(jìn)行訪問(wèn)。
關(guān)于函數(shù)聲明和變量聲明鸳谜,這篇文章講的很好: https://github.com/yygmind/blog/issues/13
另外關(guān)于閉包的理解膝藕,如例子:
function foo() {
var num = 20;
function bar() {
var result = num + 20;
return result
}
bar()
}
foo()
瀏覽器分析如下:
[站外圖片上傳中...(image-3d274e-1551101247545)]
chrome瀏覽器理解閉包是foo,那么按瀏覽器的標(biāo)準(zhǔn)是如何定義閉包的咐扭,總結(jié)為三點(diǎn):
在函數(shù)內(nèi)部定義新函數(shù)
新函數(shù)訪問(wèn)外層函數(shù)的局部變量芭挽,即訪問(wèn)外層函數(shù)環(huán)境的活動(dòng)對(duì)象屬性
新函數(shù)執(zhí)行,創(chuàng)建新的函數(shù)執(zhí)行上下文蝗肪,外層函數(shù)即為閉包
3.3 this指向
比較復(fù)雜袜爪,后面專門弄一篇文章來(lái)整理。
后面的內(nèi)容請(qǐng)往這里走 JS引擎線程的執(zhí)行過(guò)程的三個(gè)階段(二)