一、概述
js是一種非常靈活的語言,理解js引擎的執行過程對於我們學習js是非常有必要的。看了很多這方便文章,大多數是講的是事件迴圈(event loop)或者變數提升的等,並沒有全面分析其中的過程。所以覺得把這個js執行的詳細過程整理一下,幫助更好的理解js。
1.1基礎概念
js是單執行緒語言。
在瀏覽器中一個頁面永遠只有一個執行緒在執行js指令碼程式碼
js是單執行緒怨言,但是程式碼解析是非常迅速的,不會發生解析阻塞。
js是非同步執行的,通過實踐迴圈(event loop)方式實現的
暫時我們不考慮事件迴圈(event loop),我們先來看這樣一段程式碼,來確定我們是否理解js引擎的執行過程
console.log(person)
console.log(personFun)
var person = "saucxs";
console.log(person)
function personFun() {
console.log(person)
var person = "songEagle";
console.log(person)
}
personFun()
console.log(person)複製程式碼
可以自己直接使用瀏覽器看出輸出結果
首先我們來分析一下上面的程式碼,雖然很多開發人員基本上都能答出來,但是還是要囉嗦一下。
全面分析js引擎的執行過程,分為三個階段
1、語法分析
2、預編譯階段
3、執行階段
說明:瀏覽器先按照js的順序載入<script>標籤分隔的程式碼塊,js程式碼塊載入完畢之後,立刻進入到上面的三個階段,然後再按照順序找下一個程式碼塊,再繼續執行三個階段,無論是外部指令碼檔案(不非同步載入)還是內部指令碼程式碼塊,都是一樣的,並且都在同一個全域性作用域中。
二、語法分析
js的程式碼塊載入完畢之後,會首先進入到語法分析階段,該階段的主要作用:
分析該js指令碼程式碼塊的語法是否正確,如果出現不正確會向外丟擲一個語法錯誤(syntaxError),停止改js程式碼的執行,然後繼續查詢並載入下一個程式碼塊;如果語法正確,則進入到預編譯階段。
類似的語法報錯的如下圖所示:
三、預編譯階段
js程式碼塊通過語法分析階段之後,語法都正確的下回進入預編譯階段。
在分析預編譯階段之前,我們先來了解一下js的執行環境,執行環境主要由三種:
1、全域性環境(js程式碼載入完畢後,進入到預編譯也就是進入到全域性環境)
2、函式環境(函式呼叫的時候,進入到該函式環境,不同的函式,函式環境不同)
3、eval環境(不建議使用,存在安全、效能問題)
每進入到一個不同的執行環境都會建立 一個相應的執行上下文(execution context),那麼在一段js程式中一般都會建立多個執行上下文,js引擎會以棧的資料結構對這些執行進行處理,形成函式呼叫棧(call stack),棧底永遠是全域性執行上下文(global execution context),棧頂則永遠時當前的執行上下文。
3.1函式呼叫棧
什麼是函式呼叫棧?
函式呼叫棧就是使用棧存取的方式進行管理執行環境,特點是先進後出,後進後出
我們來分析一下簡答的js程式碼來理解函式呼叫棧:
function bar() {
var B_context = "bar saucxs";
function foo() {
var f_context = "foo saucxs";
}
foo()
}
bar()複製程式碼
上面程式碼塊通過語法分析後,進入預編譯階段,如圖所示
1、首先進入到全域性環境,建立全域性執行上下文(global Execution Context ),推入到stack中;
2、呼叫bar函式,進入bar函式執行環境,建立bar函式執行上下文(bar Execution Context),推入stack棧中;
3、在bar函式內部呼叫foo函式,則再進入到foo函式執行環境中,建立foo函式執行上下文(foo Execution Context),如上圖,由於foo函式內部沒有再呼叫其他函式,那麼則開始出棧;
5、foo函式執行完畢之後,棧頂foo函式執行上下文(foo Execution Context)首先出棧;
6、bar函式執行完畢,bar函式執行上下文(bar Execution Context)出棧;
7、全域性上下文(global Execution Cntext)在瀏覽器或者該標籤關閉的時候出棧。
說明:不同的執行環境執行都會進入到程式碼預編譯和執行兩個階段,語法分析則在程式碼塊載入完畢時統一檢查語法。
3.2建立執行上下文
執行上下文可以理解成當前的執行環境,與該執行環境相對應。建立執行上下文的過程中,主要是做了下面三件事,如圖所示:
1、建立變數物件(variable object)
2、建立作用域鏈(scope chain)
3、確定this的指向
3.2.1建立變數物件
建立變數物件主要是經過以下過程,如圖所示:
1、建立arguments物件,檢查當前上下文的引數,建立該物件的屬性與屬性值,僅在函式環境(非箭頭函式)中進行的,全域性環境沒有此過程。
2、檢查當前上下文的函式宣告,按照程式碼順序查詢,將找到的函式提前宣告,如果當前上下文的變數物件沒有該函式名屬性,則在該變數物件以函式名建立一個屬性,屬性值則指向該函式所在堆記憶體地址引用,如果存在,則會被新的引用覆蓋掉。
3、檢查當前上下文的變數宣告,愛去哪找程式碼順序查詢,將找到的變數提前宣告,如果當前上下文的變數物件沒有變數名屬性,則在該變數物件以變數名建立一個屬性,屬性值為undefined;如果存在,則忽略該變數宣告。
說明:在全域性環境中,window物件就是全域性執行上下文的變數物件,所有的變數和函式都是window物件的屬性方法。
所以函式宣告提前和變數宣告提升是在建立變數物件中進行的,且函式宣告優先順序高於變數宣告。
下面我們再來分析這個簡單程式碼
function fun(m,n){
var saucxs = 1;
function execution(){
console.log(saucxs)
}
}
fun(2,3)複製程式碼
這裡我們在全域性環境中呼叫fun函式,建立fun的執行上下文,這裡暫時不說作用域鏈以及this指向的問題。
funEC = {
//變數物件
VO: {
//arguments物件
arguments: {
m: undefined,
n: undefined,
length: 2
},
//execution函式
execution: <execution reference>,
//num變數
saucxs: undefined
},
//作用域鏈
scopeChain:[],
//this指向
this: window
}複製程式碼
1、funEC表示fun函式的執行上下文(fun Execution Context 簡寫為funEC);
2、funEC的變數物件中arguments屬性,上面這樣寫只是為了理解,在瀏覽器中展示以類陣列的方式展示的
3、<execution reference>表示的是execution函式在堆記憶體地址的引用
說明:建立變數物件發生在預編譯階段,還沒有進入到執行階段,該變數物件都不能訪問的,因為此時的變數物件中的變數屬性尚未賦值,值仍為undefined,只有在進行執行階段,變數中的變數屬性才進行賦值後,變數物件(Variable Object)轉為活動物件(Active Object)後,才能進行訪問,這個過程就是VO->AO過程。
3.2.2建立作用域鏈
作用域鏈由當前執行環境的變數物件(未進入到執行階段前)與上層環境的一系列活動物件組成,保證了當前執行還款對符合訪問許可權的變數和函式有序訪問。
理解清楚作用域鏈可以幫助我們理解js很多問題包括閉包問題等,下面我們結合一個例子來理解一下作用域鏈。
var num = 30;
function test() {
var a = 10;
function innerTest() {
var b = 20;
return a + b
}
innerTest()
}
test()複製程式碼
在上面例子中,當執行到呼叫innerTest函式,進入到innerTest函式環境。全域性執行上下文和test函式執行上下文已進入到執行階段,innerTest函式執行上下文在預編譯階段建立變數物件,所以他們的活動物件和變數物件分別是AO(global),AO(test)和VO(innerTest),而innerTest的作用域鏈由當前執行環境的變數物件(未進入到執行階段前)與上層環境的一系列活動物件組成,如下:
innerTestEC = {
//變數物件
VO: {b: undefined},
//作用域鏈
scopeChain: [VO(innerTest), AO(test), AO(global)],
//this指向
this: window
}複製程式碼
我們這裡可以直接使用陣列表示作用域鏈,作用域鏈的活動物件或者變數物件可以直接理解成作用域。
1、作用域鏈的第一項永遠是當前作用域(當前上下文的變數物件或者活動物件);
2、最後一項永遠是全域性作用域(全域性上下文的活動物件);
3、作用域鏈保證了變數和函式的有序訪問,查詢方式是沿著作用域鏈從左至右查詢變數或者函式,找到則會停止找,找不到則一直查詢全域性作用域,再找不到就會排除錯誤。
3.2.3閉包
什麼是閉包?思考一下
看一下簡單的例子
function foo() {
var num = 20;
function bar() {
var result = num + 20;
return result
}
bar()
}
foo()複製程式碼
因為對於閉包的有很多的不同理解,包括我看一些書籍(js高階程式設計),我這直接以瀏覽器解析,以瀏覽器的閉包為準來分析閉包,如圖
如圖所示,谷歌瀏覽器理解的閉包是foo,那麼按照瀏覽器的標準是如何定義的閉包,自己總結為三點:
1、在函式內部定義新函式
2、新函式訪問外層函式的區域性變數,即訪問外層函式環境的活動物件屬性
3、新函式執行,建立新函式的執行上下文,外層函式即為閉包
3.2.4確定this指向
1、在全域性環境下,全域性執行的上下文中變數物件的this屬性指向為window;
2、在函式環境下的this指向比較靈活,需要根據執行環境和執行方法確定,列舉典型例子來分析
四、總結
由於涉及到的內容過多,下一次將第三階段(執行階段)單獨分離出來。另開出新文章詳細分析,主要介紹js執行階段中的同步任務執行和非同步任務執行機制(事件迴圈(Event Loop))。
五、參考書籍
- 你不知道的javascript(上卷)