js是一種很是靈活的語言,理解js引擎的執行過程對咱們學習javascript很是重要,可是網上講解js引擎的文章也大可能是淺嘗輒止或者只局部分析,例如只分析事件循環(Event Loop)或者變量提高等等,並無全面深刻的分析其中過程。因此我一直想把js執行的詳細過程整理成一個較爲詳細的知識體系,幫助咱們理解和總體認識js。javascript
在分析以前咱們先了解如下基礎概念:java
javascript是單線程語言git
在瀏覽器中一個頁面永遠只有一個線程在執行js腳本代碼(在不主動開啓新線程的狀況下)。github
javascript是單線程語言,可是代碼解析卻十分的快速,不會發生解析阻塞。chrome
javascript是異步執行的,經過事件循環(Event Loop)的方式實現。數組
下面咱們先經過一段較爲簡單的代碼(暫不存在事件循環(Event Loop))來檢驗咱們對js引擎執行過程的理解是否正確,以下:瀏覽器
<script> |
咱們能夠先分析上面的代碼,按本身的理解分析輸出的順序是什麼,而後在瀏覽器執行一次,結果同樣的話,那麼表明你已經對js引擎執行過程有了正確的理解;若是不是,則表明還存在模糊或者概念不清晰等問題。結果咱們不在這裏進行討論,咱們利用上面簡單的例子全面分析js引擎執行過程,相信在理解該過程後咱們就不可貴出結果的,js引擎執行過程分爲三個階段:安全
語法分析閉包
預編譯階段異步
執行階段
注:瀏覽器首先按順序加載由<script>
標籤分割的js代碼塊,加載js代碼塊完畢後,馬上進入以上三個階段,而後再按順序查找下一個代碼塊,再繼續執行以上三個階段,不管是外部腳本文件(不異步加載)仍是內部腳本代碼塊,都是同樣的原理,而且都在同一個全局做用域中。
js腳本代碼塊加載完畢後,會首先進入語法分析階段。該階段主要做用是:
分析該js腳本代碼塊的語法是否正確,若是出現不正確,則向外拋出一個語法錯誤(SyntaxError),中止該js代碼塊的執行,而後繼續查找並加載下一個代碼塊;若是語法正確,則進入預編譯階段
語法錯誤報錯以下圖:
js代碼塊經過語法分析階段後,語法正確則進入預編譯階段。在分析預編譯階段以前,咱們先了解一下js的運行環境,運行環境主要有三種:
全局環境(JS代碼加載完畢後,進入代碼預編譯即進入全局環境)
函數環境(函數調用執行時,進入該函數環境,不一樣的函數則函數環境不一樣)
eval(不建議使用,會有安全,性能等問題)
每進入一個不一樣的運行環境都會建立一個相應的執行上下文(Execution Context),那麼在一段JS程序中通常都會建立多個執行上下文,js引擎會以棧的方式對這些執行上下文進行處理,造成函數調用棧(call stack),棧底永遠是全局執行上下文(Global Execution Context),棧頂則永遠是當前執行上下文。
函數調用棧就是使用棧存取的方式進行管理運行環境,特色是先進後出,後進先出。
咱們分析下段簡單的JS腳本代碼來理解函數調用棧:
function bar() { |
上面的代碼塊經過語法分析後,進入預編譯階段,以下圖:
首先進入全局環境,建立全局執行上下文(Global Execution Context),推入stack棧中
調用bar函數,進入bar函數運行環境,建立bar函數執行上下文(bar Execution Context),推入stack棧中
在bar函數內部調用foo函數,則再進入foo函數運行環境,建立foo函數執行上下文(foo Execution Context),推入stack棧中
此刻棧底是全局執行上下文(Global Execution Context),棧頂是foo函數執行上下文(foo Execution Context),如上圖,因爲foo函數內部沒有再調用其餘函數,那麼則開始出棧
foo函數執行完畢後,棧頂foo函數執行上下文(foo Execution Context)首先出棧
bar函數執行完畢,bar函數執行上下文(bar Execution Context)出棧
Global Execution Context則在瀏覽器或者該標籤頁關閉時出棧。
注:不一樣的運行環境執行都會進入代碼預編譯和執行兩個階段,語法分析則在代碼塊加載完畢時統一檢驗語法
執行上下文可理解爲當前的執行環境,與該運行環境相對應。建立執行上下文的過程當中,主要作了如下三件事件,如圖:
建立變量對象(Variable Object)
創建做用域鏈(Scope Chain)
肯定this的指向
建立變量對象主要通過如下幾個過程,如圖:
建立arguments對象,檢查當前上下文中的參數,創建該對象的屬性與屬性值,僅在函數環境(非箭頭函數)中進行,全局環境沒有此過程
檢查當前上下文的函數聲明,按代碼順序查找,將找到的函數提早聲明,若是當前上下文的變量對象沒有該函數名屬性,則在該變量對象以函數名創建一個屬性,屬性值則爲指向該函數所在堆內存地址的引用,若是存在,則會被新的引用覆蓋。
檢查當前上下文的變量聲明,按代碼順序查找,將找到的變量提早聲明,若是當前上下文的變量對象沒有該變量名屬性,則在該變量對象以變量名創建一個屬性,屬性值爲undefined;若是存在,則忽略該變量聲明
注:在全局環境中,window對象就是全局執行上下文的變量對象,全部的變量和函數都是window對象的屬性方法。
因此函數聲明提早和變量聲明提高是在建立變量對象中進行的,且函數聲明優先級高於變量聲明。
咱們分析一段簡單的代碼,幫助咱們理解該過程,以下:
function fun(a, b) { |
這裏咱們在全局環境調用fun函數,建立fun執行上下文,這裏爲了方便你們理解,暫時不講解做用域鏈以及this指向,以下:
funEC = { |
funEC表示fun函數的執行上下文(fun Execution Context簡寫爲funEC)
funE的變量對象中arguments屬性,上面的寫法僅爲了方便你們理解,可是在瀏覽器中展現是以類數組的方式展現的
<test reference>
表示test函數在堆內存地址的引用
注:建立變量對象發生在預編譯階段,但還沒有進入執行階段,該變量對象都是不能訪問的,由於此時的變量對象中的變量屬性還沒有賦值,值仍爲undefined,只有進入執行階段,變量對象中的變量屬性進行賦值後,變量對象(Variable Object)轉爲活動對象(Active Object)後,才能進行訪問,這個過程就是VO –> AO過程。
做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
理清做用域鏈能夠幫助咱們理解js不少問題包括閉包問題等,下面咱們結合一個簡單的例子來理解做用域鏈,以下:
var num = 30; |
在上面的例子中,當執行到調用innerTest函數,進入innerTest函數環境。全局執行上下文和test函數執行上下文已進入執行階段,innerTest函數執行上下文在預編譯階段建立變量對象,因此他們的活動對象和變量對象分別是AO(global),AO(test)和VO(innerTest),而innerTest的做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,以下:
innerTestEC = { |
咱們這裏直接使用數組表示做用域鏈,做用域鏈的活動對象或變量對象能夠直接理解爲做用域。
做用域鏈的第一項永遠是當前做用域(當前上下文的變量對象或活動對象);
最後一項永遠是全局做用域(全局執行上下文的活動對象);
做用域鏈保證了變量和函數的有序訪問,查找方式是沿着做用域鏈從左至右查找變量或函數,找到則會中止查找,找不到則一直查找到全局做用域,再找不到則會拋出引用錯誤。
在這裏咱們順便思考一下,什麼是閉包?
咱們先看下面一個簡單例子,以下:
function foo() { |
由於對於閉包有不少不一樣的理解,包括我看的一些書籍(例如js高級程序設計),我這裏直接以瀏覽器解析,以瀏覽器理解的閉包爲準來分析閉包,以下圖:
如上圖所示,chrome瀏覽器理解閉包是foo,那麼按瀏覽器的標準是如何定義閉包的,我總結爲三點:
在函數內部定義新函數
新函數訪問外層函數的局部變量,即訪問外層函數環境的活動對象屬性
新函數執行,建立新的函數執行上下文,外層函數即爲閉包
在全局環境下,全局執行上下文中變量對象的this屬性指向爲window;函數環境下的this指向卻較爲靈活,需根據執行環境和執行方法肯定,須要舉大量的典型例子歸納,本文先不作分析。
因爲涉及的內容過多,這裏將第三個階段(執行階段)單獨分離出來。另開新文章進行詳細分析,下篇文章主要介紹js執行階段中的同步任務執行和異步任務執行機制(事件循環(Event Loop))。本文若是錯誤,敬請指正。