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

来源:https://www.cnblogs.com/duiniweixiao/archive/2018/04/23/8918607.html
-Advertisement-
Play Games

概述 js是一種非常靈活的語言,理解js引擎的執行過程對我們學習javascript非常重要,但是網上講解js引擎的文章也大多是淺嘗輒止或者只局部分析,例如只分析事件迴圈(Event Loop)或者變數提升等等,並沒有全面深入的分析其中過程。所以我一直想把js執行的詳細過程整理成一個較為詳細的知識體 ...


概述

js是一種非常靈活的語言,理解js引擎的執行過程對我們學習javascript非常重要,但是網上講解js引擎的文章也大多是淺嘗輒止或者只局部分析,例如只分析事件迴圈(Event Loop)或者變數提升等等,並沒有全面深入的分析其中過程。所以我一直想把js執行的詳細過程整理成一個較為詳細的知識體系,幫助我們理解和整體認識js。

在分析之前我們先瞭解以下基礎概念:

  • javascript是單線程語言

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

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

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • jsp中引入: <OBJECT id=WebBrowser classid=CLSID:8856F961-340A-11D0-A96B-00C04FD705A2 height=0 width=0></OBJECT> jsp中引入樣式: 法二:直接全部引進去,做相關內容的替換(有提示!)調用方法即可 ...
  • 前言 小程式開發的過程中,如果你涉及到文件的上傳,就需要使用微信提供的API去上傳文件: 官方文檔的解釋這裡就不多介紹了,主要看一下這個方法具體如何使用以及為什麼這樣使用。 正文 我們可以先看一下該API的參數說明: 其實wx.uploadFile的操作是你把要請求的數據以及要請求的伺服器URL傳遞 ...
  • 我們在做數據提交的時候經常用到表單驗證,如果遇到表單元素有沒填的選項,一般都會禁止表單提交 如果表單需要驗證的數據比較多,有些必填的欄位為空 提交不了 但是沒有定位到未填項的位置 導致用戶懵逼 不知道為什麼提交不了 這個時候,我們可以給未填的表單項加foucs() 例如上圖的代碼,這樣游標就可以定位 ...
  • 0. 瀏覽器渲染原理: 1. 瀏覽器宿主環境層面: 2. 網路層面: 3. 代碼層面: ...
  • 原型鏈是js面向對象的基礎,非常重要。 一,創建對象的幾種方法: 1,字面量 var o1 = { name:'o1' }; 2,構造函數 var M = function(name){ this.name = name; }; var o2 = new M('o2'); var a = {} 其實 ...
  • 1.模板名片發送後不顯示內容?(如第一張圖) 經過查看官方文檔,是data數據格式問題,小程式端傳給後端的data數據被服務端解析出了一點問題(data裡面的字元串加入了"\")。現在後端將數據從新做了清洗。已解決。解決後的展示如第二張圖。 2.上傳圖片一直失敗。 解決答案相關鏈接:https:// ...
  • 概述 理解柯里化函數,需要有閉包的基礎,只有徹底理解閉包後才能理解柯里化,如果尚未理解閉包,建議閱讀上文js引擎的執行過程(一);如果理解了閉包再研究柯里化函數,則會大大的加深你對閉包理解,並且更清楚的認識到閉包的應用場景,那麼如果在面試時候問到閉包,你就可以侃侃而談了;並且理解柯里化函數會在很大的 ...
  • 當代碼在執行環境中執行時,會創建一個作用域鏈。作用域鏈本質是一個指向變數對象的指針列表。 如果執行環境是函數,則將其活動對象(最開始時只包含一個變數->argument對象)作為變數對象。ps:argument對象在全局環境中是不存在的. (基於2條件下)作用域鏈中的下一個變數對象來自外部環境,而再 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...