javascript引擎執行的過程的理解--語法分析和預編譯階段


一、概述

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()

上面代碼塊通過語法分析后,進入預編譯階段,如圖所示

stack

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創建執行上下文

執行上下文可以理解成當前的執行環境,與該運行環境相對應。創建執行上下文的過程中,主要是做了下面三件事,如圖所示:

EC

1、創建變量對象(variable object)

2、創建作用域鏈(scope chain)

3、確定this的指向

 

3.2.1創建變量對象

創建變量對象主要是經過以下過程,如圖所示:

VO

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(上卷)

 

同步songEagle(首發):http://www.chengxinsong.cn/post/23


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM