(轉載)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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...