js是一種很是靈活的語言,理解js引擎的執行過程對於咱們學習js是很是有必要的。看了不少這方便文章,大多數是講的是事件循環(event loop)或者變量提高的等,並無全面分析其中的過程。因此以爲把這個js執行的詳細過程整理一下,幫助更好的理解js。javascript
js是單線程語言。java
在瀏覽器中一個頁面永遠只有一個線程在執行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引擎的執行過程,分爲三個階段函數
一、語法分析oop
二、預編譯階段
三、執行階段
說明:瀏覽器先按照js的順序加載<script>標籤分隔的代碼塊,js代碼塊加載完畢以後,馬上進入到上面的三個階段,而後再按照順序找下一個代碼塊,再繼續執行三個階段,不管是外部腳本文件(不異步加載)仍是內部腳本代碼塊,都是同樣的,而且都在同一個全局做用域中。
js的代碼塊加載完畢以後,會首先進入到語法分析階段,該階段的主要做用:
分析該js腳本代碼塊的語法是否正確,若是出現不正確會向外拋出一個語法錯誤(syntaxError),中止改js代碼的執行,而後繼續查找並加載下一個代碼塊;若是語法正確,則進入到預編譯階段。
相似的語法報錯的以下圖所示:
js代碼塊經過語法分析階段以後,語法都正確的下回進入預編譯階段。
在分析預編譯階段以前,咱們先來了解一下js的運行環境,運行環境主要由三種:
一、全局環境(js代碼加載完畢後,進入到預編譯也就是進入到全局環境)
二、函數環境(函數調用的時候,進入到該函數環境,不一樣的函數,函數環境不一樣)
三、eval環境(不建議使用,存在安全、性能問題)
每進入到一個不一樣的運行環境都會建立 一個相應的執行上下文(execution context),那麼在一段js程序中通常都會建立多個執行上下文,js引擎會以棧的數據結構對這些執行進行處理,造成函數調用棧(call stack),棧底永遠是全局執行上下文(global execution context),棧頂則永遠時當前的執行上下文。
什麼是函數調用棧?
函數調用棧就是使用棧存取的方式進行管理運行環境,特色是先進後出,後進後出
咱們來分析一下簡答的js代碼來理解函數調用棧:
function bar() { var B_context = "bar saucxs"; function foo() { var f_context = "foo saucxs"; } foo() } bar()
上面代碼塊經過語法分析後,進入預編譯階段,如圖所示
一、首先進入到全局環境,建立全局執行上下文(global Execution Context ),推入到stack中;
二、調用bar函數,進入bar函數運行環境,建立bar函數執行上下文(bar Execution Context),推入stack棧中;
三、在bar函數內部調用foo函數,則再進入到foo函數運行環境中,建立foo函數執行上下文(foo Execution Context),如上圖,因爲foo函數內部沒有再調用其餘函數,那麼則開始出棧;
五、foo函數執行完畢以後,棧頂foo函數執行上下文(foo Execution Context)首先出棧;
六、bar函數執行完畢,bar函數執行上下文(bar Execution Context)出棧;
七、全局上下文(global Execution Cntext)在瀏覽器或者該標籤關閉的時候出棧。
說明:不一樣的運行環境執行都會進入到代碼預編譯和執行兩個階段,語法分析則在代碼塊加載完畢時統一檢查語法。
執行上下文能夠理解成當前的執行環境,與該運行環境相對應。建立執行上下文的過程當中,主要是作了下面三件事,如圖所示:
一、建立變量對象(variable object)
二、建立做用域鏈(scope chain)
三、肯定this的指向
建立變量對象主要是通過如下過程,如圖所示:
一、建立arguments對象,檢查當前上下文的參數,創建該對象的屬性與屬性值,僅在函數環境(非箭頭函數)中進行的,全局環境沒有此過程。
二、檢查當前上下文的函數聲明,按照代碼順序查找,將找到的函數提早聲明,若是當前上下文的變量對象沒有該函數名屬性,則在該變量對象以函數名創建一個屬性,屬性值則指向該函數所在堆內存地址引用,若是存在,則會被新的引用覆蓋掉。
三、檢查當前上下文的變量聲明,愛去哪找代碼順序查找,將找到的變量提早聲明,若是當前上下文的變量對象沒有變量名屬性,則在該變量對象以變量名創建一個屬性,屬性值爲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 }
一、funEC表示fun函數的執行上下文(fun Execution Context 簡寫爲funEC);
二、funEC的變量對象中arguments屬性,上面這樣寫只是爲了理解,在瀏覽器中展現以類數組的方式展現的
三、<execution reference>表示的是execution函數在堆內存地址的引用
說明:建立變量對象發生在預編譯階段,尚未進入到執行階段,該變量對象都不能訪問的,由於此時的變量對象中的變量屬性還沒有賦值,值仍爲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高級程序設計),我這直接以瀏覽器解析,以瀏覽器的閉包爲準來分析閉包,如圖
如圖所示,谷歌瀏覽器理解的閉包是foo,那麼按照瀏覽器的標準是如何定義的閉包,本身總結爲三點:
一、在函數內部定義新函數
二、新函數訪問外層函數的局部變量,即訪問外層函數環境的活動對象屬性
三、新函數執行,建立新函數的執行上下文,外層函數即爲閉包
一、在全局環境下,全局執行的上下文中變量對象的this屬性指向爲window;
二、在函數環境下的this指向比較靈活,須要根據執行環境和執行方法肯定,列舉典型例子來分析
因爲涉及到的內容過多,下一次將第三階段(執行階段)單獨分離出來。另開出新文章詳細分析,主要介紹js執行階段中的同步任務執行和異步任務執行機制(事件循環(Event Loop))。
你不知道的javascript(上卷)
同步songEagle(首發):(http://www.chengxinsong.cn/po...