成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

手把手教會你JavaScript引擎如何執行JavaScript代碼

開發 前端
今天我們了解了 JavaScript 代碼的運行過程,該過程分為語法分析階段、編譯階段、執行階段三個階段。

[[439546]]

JavaScript 在運行過程中與其他語言有所不一樣,如果不理解 JavaScript 的詞法環境、執行上下文等內容,很容易會在開發過程中產生 Bug,比如this指向和預期不一致、某個變量不知道為什么被改了,等等。所以今天我們就來聊一聊 JavaScript 代碼的運行過程。

大家都知道,JavaScript 代碼是需要在 JavaScript 引擎中運行的。我們在說到 JavaScript 運行的時候,常常會提到執行環境、詞法環境、作用域、執行上下文、閉包等內容。這些概念看起來都差不多,卻好像又不大容易區分清楚,它們分別都在描述什么呢?

這些詞語都是與 JavaScript 引擎執行代碼的過程有關,為了搞清楚這些概念之間的區別,我們可以回顧下 JavaScript 代碼運行過程中的各個階段。

JavaScript 代碼運行的各個階段

JavaScript 是弱類型語言,在運行時才能確定變量類型。JavaScript 引擎在執行 JavaScript 代碼時,也會從上到下進行詞法分析、語法分析、語義分析等處理,并在代碼解析完成后生成 AST(抽象語法樹),最終根據 AST 生成 CPU 可以執行的機器碼并執行。

這個過程,我們稱之為語法分析階段。除了語法分析階段,JavaScript 引擎在執行代碼時還會進行其他的處理。以 V8 引擎為例,在 V8 引擎中 JavaScript 代碼的運行過程主要分成三個階段。

  1. 語法分析階段。該階段會對代碼進行語法分析,檢查是否有語法錯誤(SyntaxError),如果發現語法錯誤,會在控制臺拋出異常并終止執行。
  2. 編譯階段。該階段會進行執行上下文(Execution Context)的創建,包括創建變量對象、建立作用域鏈、確定 this 的指向等。每進入一個不同的運行環境時,V8 引擎都會創建一個新的執行上下文。
  3. 執行階段。將編譯階段中創建的執行上下文壓入調用棧,并成為正在運行的執行上下文,代碼執行結束后,將其彈出調用棧。

其中,語法分析階段屬于編譯器通用內容,就不再贅述。前面提到的執行環境、詞法環境、作用域、執行上下文等內容都是在編譯和執行階段中產生的概念。

執行上下文的創建

執行上下文的創建離不開 JavaScript 的運行環境,JavaScript 運行環境包括全局環境、函數環境和eval,其中全局環境和函數環境的創建過程如下:

  1. 第一次載入 JavaScript 代碼時,首先會創建一個全局環境。全局環境位于最外層,直到應用程序退出后(例如關閉瀏覽器和網頁)才會被銷毀。
  2. 每個函數都有自己的運行環境,當函數被調用時,則會進入該函數的運行環境。當該環境中的代碼被全部執行完畢后,該環境會被銷毀。不同的函數運行環境不一樣,即使是同一個函數,在被多次調用時也會創建多個不同的函數環境。

在不同的運行環境中,變量和函數可訪問的其他數據范圍不同,環境的行為(比如創建和銷毀)也有所區別。而每進入一個不同的運行環境時,JavaScript 都會創建一個新的執行上下文,該過程包括:

  • 建立作用域鏈(Scope Chain);
  • 創建變量對象(Variable Object,簡稱 VO);
  • 確定 this 的指向。

由于建立作用域鏈過程中會涉及變量對象的概念,因此我們先來看看變量對象的創建,再看建立作用域鏈和確定 this 的指向。

創建變量對象

變量對象(VO)

每個執行上下文都會有一個關聯的變量對象,該對象上會保存這個上下文中定義的所有變量和函數。

在瀏覽器中,全局環境的變量對象是window對象,因此所有的全局變量和函數都是作為window對象的屬性和方法創建的。相應的,在 Node 中全局環境的變量對象則是global對象。

創建VO的過程

創建變量對象將會創建arguments對象(僅函數環境下),同時會檢查當前上下文的函數聲明和變量聲明。

  • 對于變量聲明:此時會給變量分配內存,并將其初始化為undefined(該過程只進行定義聲明,執行階段才執行賦值語句)。
  • 對于函數聲明:此時會在內存里創建函數對象,并且直接初始化為該函數對象。

變量聲明和函數聲明的處理過程,便是我們常說的變量提升和函數提升,其中函數聲明提升會優先于變量聲明提升。因為變量提升容易帶來變量在預期外被覆蓋掉的問題,同時還可能導致本應該被銷毀的變量沒有被銷毀等情況。因此 ES6 中引入了let和const關鍵字,從而使 JavaScript 也擁有了塊級作用域。

作用域

在各類編程語言中,作用域分為靜態作用域和動態作用域。JavaScript 采用的是詞法作用域(Lexical Scoping),也就是靜態作用域。詞法作用域中的變量,在編譯過程中會產生一個確定的作用域。

詞法作用域中的變量,在編譯過程中會產生一個確定的作用域,這個作用域即當前的執行上下文,在 ES5 后我們使用詞法環境(Lexical Environment)替代作用域來描述該執行上下文。因此,詞法環境可理解為我們常說的作用域,同樣也指當前的執行上下文(注意,是當前的執行上下文)。

在 JavaScript 中,詞法環境又分為詞法環境(Lexical Environment)和變量環境(Variable Environment)兩種,其中:

  • 變量環境用來記錄var/function等變量聲明;
  • 詞法環境是用來記錄let/const/class等變量聲明。

也就是說,創建變量過程中會進行函數提升和變量提升,JavaScript 會通過詞法環境來記錄函數和變量聲明。通過使用兩個詞法環境(而不是一個)分別記錄不同的變量聲明內容,JavaScript 實現了支持塊級作用域的同時,不影響原有的變量聲明和函數聲明。

這就是創建變量的過程,它屬于執行上下文創建中的一環。創建變量的過程會產生作用域,作用域也被稱為詞法環境。

建立作用域鏈

作用域鏈,就是將各個作用域通過某種方式連接在一起。作用域就是詞法環境,而詞法環境由兩個成員組成。

  1. 環境記錄(Environment Record):用于記錄自身詞法環境中的變量對象。
  2. 外部詞法環境引用(Outer Lexical Environment):記錄外層詞法環境的引用。

通過外部詞法環境的引用,作用域可以層層拓展,建立起從里到外延伸的一條作用域鏈。當某個變量無法在自身詞法環境記錄中找到時,可以根據外部詞法環境引用向外層進行尋找,直到最外層的詞法環境中外部詞法環境引用為null,這便是作用域鏈的變量查詢。

JavaScript 代碼運行過程分為定義期和執行期,前面提到的編譯階段則屬于定義期,代碼示例如下:

  1. function foo() { // 定義全局函數foo 
  2.     console.dir(bar); 
  3.     var a = 1; 
  4.     function bar() { // 在foo函數內部定義函數bar 
  5.         a = 2; 
  6.  } 
  7. console.dir(foo); 
  8. foo(); 

前面我們說到,JavaScript 使用的是靜態作用域,因此函數的作用域在定義期已經決定了。在上面的例子中,全局函數foo創建了一個foo的[[scope]]屬性,包含了全局[[scope]]:

  1. foo[[scope]] = [globalContext]; 

而當我們執行foo()時,也會分別進入foo函數的定義期和執行期。

在foo函數的定義期時,函數bar的[[scope]]將會包含全局[[scope]]和foo的[[scope]]:

  1. bar[[scope]] = [fooContext, globalContext]; 

運行上述代碼,我們可以在控制臺看到符合預期的輸出:

圖片

可以看到:

  • foo的[[scope]]屬性包含了全局[[scope]]
  • bar的[[scope]]將會包含全局[[scope]]和foo的[[scope]]

也就是說,JavaScript 會通過外部詞法環境引用來創建變量對象的一個作用域鏈,從而保證對執行環境有權訪問的變量和函數的有序訪問。除了創建作用域鏈之外,在這個過程中還會對創建的變量對象做一些處理。

在編譯階段會進行變量對象(VO)的創建,該過程會進行函數聲明和變量聲明,這時候變量的值被初始化為 undefined。在代碼進入執行階段之后,JavaScript 會對變量進行賦值,此時變量對象會轉為活動對象(Active Object,簡稱 AO),轉換后的活動對象才可被訪問,這就是 VO -> AO 的過程,示例如下:

  1. function foo(a) { 
  2.     var b = 2;  
  3.     function c() {} 
  4.     var d = function() {}; 
  5.  
  6. foo(1); 

在執行foo(1)時,首先進入定義期,此時:

  • 參數變量a的值為1
  • 變量b和d初始化為undefined
  • 函數c創建函數并初始化
  1. AO = { 
  2.  arguments: { 
  3.   0: 1, 
  4.   length: 1 
  5.  }, 
  6.  a: 1, 
  7.  b: undefined, 
  8.  c: reference to function() c() {} 
  9.  d:undefined 

前面我們也有提到,進入執行期之后,會執行賦值語句進行賦值,此時變量b和d會被賦值為 2 和函數表達式:

  1. AO = { 
  2.    arguments: { 
  3.     0: 1, 
  4.     length: 1 
  5.   }, 
  6.   a: 1, 
  7.   b: 2, 
  8.   c: reference to function c(){}, 
  9.   d: reference to FunctionExpression "d" 

這就是 VO -> AO 過程。

  • 在定義期(編譯階段):該對象值仍為undefined,且處于不可訪問的狀態。
  • 進入執行期(執行階段):VO 被激活,其中變量屬性會進行賦值。

實際上在執行的時候,除了 VO 被激活,活動對象還會添加函數執行時傳入的參數和arguments這個特殊對象,因此 AO 和 VO 的關系可以用以下關系來表達:

  1. AO = VO + function parameters + arguments 

現在,我們知道作用域鏈是在進入代碼的執行階段時,通過外部詞法環境引用來創建的。總結如下:

  • 在編譯階段,JavaScript 在創建執行上下文的時候會先創建變量對象(VO);
  • 在執行階段,變量對象(VO)被激活為活動對象( AO),函數內部的變量對象通過外部詞法環境的引用創建作用域鏈。

通過作用域鏈,我們可以在函數內部可以直接讀取外部以及全局變量,但外部環境是無法訪問內部函數里的變量。示例如下:

  1. function foo() { 
  2.   var a = 1; 
  3. foo(); 
  4. console.log(a); // undefined 

我們在全局環境下無法訪問函數foo中的變量a,這是因為全局函數的作用域鏈里,不含有函數foo內的作用域。

如果我們想要訪問內部函數的變量,可以通過函數foo中的函數bar返回變量a,并將函數bar返回,這樣我們在全局環境中也可以通過調用函數foo返回的函數bar,來訪問變量a:

  1. function foo() { 
  2.   var a = 1; 
  3.   function bar() { 
  4.     return a; 
  5.   } 
  6.   return bar; 
  7. var b = foo(); 
  8. console.log(b()); // 1 

當函數執行結束之后,執行期上下文將被銷毀,其中包括作用域鏈和激活對象。

在上面的實例中;當b()執行時,foo函數上下文包括作用域都已經被銷毀了,但是foo作用域下的a依然可以被訪問到;這是因為bar函數引用了foo函數變量對象中的值,此時即使創建bar函數的foo函數執行上下文被銷毀了,但它的變量對象依然會保留在 JavaScript 內存中,bar函數依然可以通過bar函數的作用域鏈找到它,并進行訪問。這就是閉包;

閉包使得我們可以從外部讀取局部變量,常見的用途包括:

  1. 用于從外部讀取其他函數內部變量的函數;
  2. 可以使用閉包來模擬私有方法;
  3. 讓這些變量的值始終保持在內存中。

注意,在使用閉包的時候,需要及時清理不再使用到的變量,否則可能導致內存泄漏問題。

確定 this 的指向

在 JavaScript 中,this指向執行當前代碼對象的所有者,可簡單理解為this指向最后調用當前代碼的那個對象。

根據 JavaScript 中函數的調用方式不同,this的指向分為以下情況。

在全局環境中,this指向全局對象(在瀏覽器中為window)

在函數內部,this的值取決于函數被調用的方式

  • 函數作為對象的方法被調用,this指向調用這個方法的對象
  • 函數用作構造函數時(使用new關鍵字),它的this被綁定到正在構造的新對象
  • 在類的構造函數中,this是一個常規對象,類中所有非靜態的方法都會被添加到this的原型中

在箭頭函數中,this指向它被創建時的環境

使用apply、call、bind等方式調用:根據 API 不同,可切換函數執行的上下文環境,即this綁定的對象

可以看到,this在不同的情況下會有不同的指向,在 ES6 箭頭函數還沒出現之前,為了能正確獲取某個運行環境下this對象,我們常常會使用以下代碼:

  1. var that = this; 
  2. var self = this; 

這樣的代碼將變量分配給this,便于使用。但是降低了代碼可讀性,不推薦使用,通過正確使用箭頭函數,我們可以更好地管理作用域。

總結

今天我們了解了 JavaScript 代碼的運行過程,該過程分為語法分析階段、編譯階段、執行階段三個階段。

在編譯階段,JavaScript會進行執行上下文的創建,在執行階段,變量對象(VO)會被激活為活動對象(AO),變量會進行賦值,此時活動對象才可被訪問。在執行結束之后,作用域鏈和活動對象均被銷毀,使用閉包可使活動對象依然被保留在內存中。這就是 JavaScript 代碼的運行過程。

 

責任編輯:姜華 來源: IT共享之家
相關推薦

2009-12-11 09:04:10

Windows搭建Li

2009-12-21 11:36:24

Linux啟動加密

2009-10-22 15:23:32

VB.NET函數

2021-06-16 09:02:43

Pythonjieba分詞Python基礎

2010-07-30 10:41:23

DB2打補丁

2020-08-12 09:07:53

Python開發爬蟲

2021-03-12 10:01:24

JavaScript 前端表單驗證

2020-12-13 11:38:09

Go語言clac包

2017-07-19 13:27:44

前端Javascript模板引擎

2014-01-22 09:19:57

JavaScript引擎

2020-03-06 13:09:42

JavaScriptSublimeTextNode.js

2021-04-02 10:01:00

JavaScript前端Web項目

2024-11-05 16:40:24

JavaScript搜索引擎

2022-02-25 09:41:05

python搜索引擎

2021-08-24 10:02:21

JavaScript網頁搜索 前端

2020-10-26 15:09:35

Python爬蟲網頁數據

2022-02-17 10:26:17

JavaScript掃雷游戲前端

2022-10-30 10:31:42

i2ccpuftrace

2022-07-27 08:16:22

搜索引擎Lucene

2022-12-07 08:42:35

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久99精品久久久久久 | 欧美一级黄色片 | 精品国产乱码久久久久久丨区2区 | 国产精品国产三级国产aⅴ无密码 | 在线免费激情视频 | 日本 欧美 三级 高清 视频 | 超碰人人插| 国产精品国产a级 | 一区二区在线 | 天天操天天射天天 | 日韩久久久一区二区 | 91视频免费在观看 | 特级生活片| a毛片| 欧美中文字幕一区二区三区 | 欧美日韩亚洲一区二区 | 久久精品亚洲成在人线av网址 | 亚洲精品综合一区二区 | 亚洲一区二区三区在线 | 999久久久| 色屁屁在线观看 | 国产 日韩 欧美 制服 另类 | 黄色精品 | 国产三区四区 | 欧美一区二区成人 | 欧美性另类 | 国产欧美精品一区二区 | 日韩精品一区二区不卡 | www成人免费视频 | 精品无码久久久久久国产 | 黄色片亚洲 | 成人免费小视频 | 五月免费视频 | 日韩欧美网 | 成年人网站在线观看视频 | 欧美舔穴| 天堂一区二区三区 | 97在线超碰| 精品国产乱码久久久久久丨区2区 | 国产成人精品一区二区三区视频 | 国产精品久久一区 |