(轉載)js引擎的執行過程(一)

概述

js是一種很是靈活的語言,理解js引擎的執行過程對咱們學習javascript很是重要,可是網上講解js引擎的文章也大可能是淺嘗輒止或者只局部分析,例如只分析事件循環(Event Loop)或者變量提高等等,並無全面深刻的分析其中過程。因此我一直想把js執行的詳細過程整理成一個較爲詳細的知識體系,幫助咱們理解和總體認識js。javascript

在分析以前咱們先了解如下基礎概念:java

  • javascript是單線程語言git

    在瀏覽器中一個頁面永遠只有一個線程在執行js腳本代碼(在不主動開啓新線程的狀況下)。github

  • javascript是單線程語言,可是代碼解析卻十分的快速,不會發生解析阻塞。chrome

    javascript是異步執行的,經過事件循環(Event Loop)的方式實現。數組

下面咱們先經過一段較爲簡單的代碼(暫不存在事件循環(Event Loop))來檢驗咱們對js引擎執行過程的理解是否正確,以下:瀏覽器

<script>
console.log(fun)

console.log(person)
</script>

<script>
console.log(person)

console.log(fun)

var person = "Eric";

console.log(person)

function fun() {
console.log(person)
var person = "Tom";
console.log(person)
}

fun()

console.log(person)
</script>

 

咱們能夠先分析上面的代碼,按本身的理解分析輸出的順序是什麼,而後在瀏覽器執行一次,結果同樣的話,那麼表明你已經對js引擎執行過程有了正確的理解;若是不是,則表明還存在模糊或者概念不清晰等問題。結果咱們不在這裏進行討論,咱們利用上面簡單的例子全面分析js引擎執行過程,相信在理解該過程後咱們就不可貴出結果的,js引擎執行過程分爲三個階段:安全

  1. 語法分析閉包

  2. 預編譯階段異步

  3. 執行階段

注:瀏覽器首先按順序加載由<script>標籤分割的js代碼塊,加載js代碼塊完畢後,馬上進入以上三個階段,而後再按順序查找下一個代碼塊,再繼續執行以上三個階段,不管是外部腳本文件(不異步加載)仍是內部腳本代碼塊,都是同樣的原理,而且都在同一個全局做用域中。

 

語法分析

js腳本代碼塊加載完畢後,會首先進入語法分析階段。該階段主要做用是:

分析該js腳本代碼塊的語法是否正確,若是出現不正確,則向外拋出一個語法錯誤(SyntaxError),中止該js代碼塊的執行,而後繼續查找並加載下一個代碼塊;若是語法正確,則進入預編譯階段

語法錯誤報錯以下圖:
syntax

 

預編譯階段

js代碼塊經過語法分析階段後,語法正確則進入預編譯階段。在分析預編譯階段以前,咱們先了解一下js的運行環境,運行環境主要有三種:

  • 全局環境(JS代碼加載完畢後,進入代碼預編譯即進入全局環境)

  • 函數環境(函數調用執行時,進入該函數環境,不一樣的函數則函數環境不一樣)

  • eval(不建議使用,會有安全,性能等問題)

每進入一個不一樣的運行環境都會建立一個相應的執行上下文(Execution Context),那麼在一段JS程序中通常都會建立多個執行上下文,js引擎會以棧的方式對這些執行上下文進行處理,造成函數調用棧(call stack),棧底永遠是全局執行上下文(Global Execution Context),棧頂則永遠是當前執行上下文。

 

函數調用棧

函數調用棧就是使用棧存取的方式進行管理運行環境,特色是先進後出,後進先出

咱們分析下段簡單的JS腳本代碼來理解函數調用棧:

function bar() {
var B_context = "Bar EC";

function foo() {
var f_context = "foo EC";
}

foo()
}

bar()

上面的代碼塊經過語法分析後,進入預編譯階段,以下圖:
stack

  1. 首先進入全局環境,建立全局執行上下文(Global Execution Context),推入stack棧中

  2. 調用bar函數,進入bar函數運行環境,建立bar函數執行上下文(bar Execution Context),推入stack棧中

  3. 在bar函數內部調用foo函數,則再進入foo函數運行環境,建立foo函數執行上下文(foo Execution Context),推入stack棧中

  4. 此刻棧底是全局執行上下文(Global Execution Context),棧頂是foo函數執行上下文(foo Execution Context),如上圖,因爲foo函數內部沒有再調用其餘函數,那麼則開始出棧

  5. foo函數執行完畢後,棧頂foo函數執行上下文(foo Execution Context)首先出棧

  6. bar函數執行完畢,bar函數執行上下文(bar Execution Context)出棧

  7. Global Execution Context則在瀏覽器或者該標籤頁關閉時出棧。

注:不一樣的運行環境執行都會進入代碼預編譯和執行兩個階段,語法分析則在代碼塊加載完畢時統一檢驗語法

 

建立執行上下文

執行上下文可理解爲當前的執行環境,與該運行環境相對應。建立執行上下文的過程當中,主要作了如下三件事件,如圖:
EC

  1. 建立變量對象(Variable Object)

  2. 創建做用域鏈(Scope Chain)

  3. 肯定this的指向

 

建立變量對象

建立變量對象主要通過如下幾個過程,如圖:
VO

  1. 建立arguments對象,檢查當前上下文中的參數,創建該對象的屬性與屬性值,僅在函數環境(非箭頭函數)中進行,全局環境沒有此過程

  2. 檢查當前上下文的函數聲明,按代碼順序查找,將找到的函數提早聲明,若是當前上下文的變量對象沒有該函數名屬性,則在該變量對象以函數名創建一個屬性,屬性值則爲指向該函數所在堆內存地址的引用,若是存在,則會被新的引用覆蓋。

  3. 檢查當前上下文的變量聲明,按代碼順序查找,將找到的變量提早聲明,若是當前上下文的變量對象沒有該變量名屬性,則在該變量對象以變量名創建一個屬性,屬性值爲undefined;若是存在,則忽略該變量聲明

注:在全局環境中,window對象就是全局執行上下文的變量對象,全部的變量和函數都是window對象的屬性方法。

因此函數聲明提早和變量聲明提高是在建立變量對象中進行的,且函數聲明優先級高於變量聲明。

咱們分析一段簡單的代碼,幫助咱們理解該過程,以下:

function fun(a, b) {
var num = 1;

function test() {

console.log(num)

}
}

fun(2, 3)

 

這裏咱們在全局環境調用fun函數,建立fun執行上下文,這裏爲了方便你們理解,暫時不講解做用域鏈以及this指向,以下:

funEC = {
//變量對象
VO: {
//arguments對象
arguments: {
a: undefined,
b: undefined,
length: 2
},

//test函數
test: <test reference>,

//num變量
num: undefined
},

//做用域鏈
scopeChain:[],

//this指向
this: window
}
  • funEC表示fun函數的執行上下文(fun Execution Context簡寫爲funEC)

  • funE的變量對象中arguments屬性,上面的寫法僅爲了方便你們理解,可是在瀏覽器中展現是以類數組的方式展現的

  • <test reference>表示test函數在堆內存地址的引用

注:建立變量對象發生在預編譯階段,但還沒有進入執行階段,該變量對象都是不能訪問的,由於此時的變量對象中的變量屬性還沒有賦值,值仍爲undefined,只有進入執行階段,變量對象中的變量屬性進行賦值後,變量對象(Variable Object)轉爲活動對象(Active Object)後,才能進行訪問,這個過程就是VO –> AO過程。

 

創建做用域鏈

做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

理清做用域鏈能夠幫助咱們理解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
}

咱們這裏直接使用數組表示做用域鏈,做用域鏈的活動對象或變量對象能夠直接理解爲做用域。

  • 做用域鏈的第一項永遠是當前做用域(當前上下文的變量對象或活動對象);

  • 最後一項永遠是全局做用域(全局執行上下文的活動對象);

  • 做用域鏈保證了變量和函數的有序訪問,查找方式是沿着做用域鏈從左至右查找變量或函數,找到則會中止查找,找不到則一直查找到全局做用域,再找不到則會拋出引用錯誤。

在這裏咱們順便思考一下,什麼是閉包

咱們先看下面一個簡單例子,以下:

function foo() {
var num = 20;

function bar() {
var result = num + 20;

return result
}

bar()
}

foo()

由於對於閉包有不少不一樣的理解,包括我看的一些書籍(例如js高級程序設計),我這裏直接以瀏覽器解析,以瀏覽器理解的閉包爲準來分析閉包,以下圖:
閉包

如上圖所示,chrome瀏覽器理解閉包是foo,那麼按瀏覽器的標準是如何定義閉包的,我總結爲三點:

  1. 在函數內部定義新函數

  2. 新函數訪問外層函數的局部變量,即訪問外層函數環境的活動對象屬性

  3. 新函數執行,建立新的函數執行上下文,外層函數即爲閉包


 

肯定this指向

在全局環境下,全局執行上下文中變量對象的this屬性指向爲window;函數環境下的this指向卻較爲靈活,需根據執行環境和執行方法肯定,須要舉大量的典型例子歸納,本文先不作分析。

 

總結

因爲涉及的內容過多,這裏將第三個階段(執行階段)單獨分離出來。另開新文章進行詳細分析,下篇文章主要介紹js執行階段中的同步任務執行和異步任務執行機制(事件循環(Event Loop))。本文若是錯誤,敬請指正。

 [原址連接](https://heyingye.github.io/2018/03/19/js%E5%BC%95%E6%93%8E%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%EF%BC%88%E4%B8%80%EF%BC%89/)

參考書籍

    • 你不知道的javascript(上卷)
相關文章
相關標籤/搜索