深入瞭解JavaScript引擎如何執行JS程式碼

2022-03-29 13:01:37
本篇文章給大家帶來了關於的相關知識,其中主要介紹了js引擎如何執行js程式碼的相關問題,js引擎在執行js程式碼時,也會從上到下進行詞法分析、語法分析、語意分析等處理,並在程式碼解析完成後生成AST,希望對大家有幫助。

相關推薦:

我們大概經常能聽到「執行環境」、「作用域」、「原型(鏈)」、「執行上下文」等內容,它們都在描述什麼?

JS程式碼的執行

我們知道了js是弱型別語言,在執行時才確定變數型別。js引擎在執行js程式碼時,也會從上到下進行 詞法分析語法分析語意分析 等處理,並在程式碼解析完成後生成AST(抽象語法樹),最終根據AST生成CPU可以執行的機器碼並執行。

除此之外,JS引擎在執行程式碼時還會進行其它處理,如 V8 中還有兩個階段:

  • 編譯階段:該階段會進行執行上下文的建立,包括建立變數物件(VO)(此時會被初始化為undefined)、建立作用域鏈、確定 this 指向等。每進入一個不同的執行環境。V8 都會建立一個新的執行上下文。
  • 執行階段:將編譯階段中建立的執行上下文壓入呼叫棧,併成為正在執行的執行上下文。程式碼執行結束後,將其彈出呼叫棧。(這裡有一個VO - AO的過程:JavaScript對變數賦值時變數被用到,此時變數物件會轉為活動物件,轉換後的活動物件才可被存取)

這就引出了兩個概念:「執行上下文」 和 「作用域鏈」。


JavaScript執行上下文

由上面我們可以知道:當js程式碼執行一段可執行程式碼時,會建立對應的執行上下文。
首先,js中可執行程式碼對應著有一個概念:「執行環境」 —— 全域性環境、函數環境 和 eval
其次,對於每個執行上下文,都有三個重要屬性:

  • 變數物件(即「VO」)
  • 作用域鏈
  • this

我們來看兩段程式碼:

var scope="global scope";function checkscope(){
	var scope="local scope";
	function f(){
		return scope;
	}
	return f();}checkscope();
var scope="global scope";function checkscope(){
	var scope="local scope";
	function f(){
		return scope;
	}
	return f;}checkscope()();

它們會列印什麼?
scope

為什麼?答案是它們的執行上下文棧不一樣!

什麼是「執行上下文棧」?
當執行一個可執行程式碼時,就會提前做準備工作,這裡的「準備工作」,專業的說法就是「執行上下文」。但隨著可執行程式碼如函數的增多,如何管理那麼多的執行上下文呢?所以JS引擎建立了執行上下文棧的概念。
我們完全可以用陣列去模擬其行為(棧底永遠有一個全域性執行上下文globalContext)

我們定義一個EStack,首先

EStack=[globalContext];

然後來模擬第一段程式碼:

EStack.push(<checkscope> functionContext);EStack.push(<f> functionContext);EStack.pop();EStack.pop();

而第二段程式碼是這樣的:

EStack.push(<checkscope> functionContext);EStack.pop();EStack.push(<f> functionContext);EStack.pop();

究其原因,你可能需要先研究一下「閉包」的概念了!

這裡順便說下「在前端模組化」中怎麼實現「長時間儲存資料」?
快取?不。閉包!


JavaScript作用域和作用域鏈

首先,作用域是指程式中定義變數的區域。作用域規定了如何查詢變數,也就是確定了當前執行程式碼對變數的存取許可權。
作用域有兩種:靜態作用域動態作用域
JS採用的靜態作用域,也叫「詞法作用域」。函數的作用域在函數定義的時候就確定了。

由上,詞法作用域中的變數,在編譯過程中會產生一個確定的作用範圍。這個作用範圍即「當前的執行上下文」。在ES5後我們用「詞法環境」替代作用域來描述該執行上下文。詞法環境由兩個成員組成:

  • 自身詞法環境記錄:用於記錄自身詞法環境中的變數物件
  • 外部詞法環境參照:用於記錄外層詞法環境中存在的參照

我們依然來看一個例子:

var value=1;function foo(){
	console.log(value);}function bar(){
	var value=2;
	foo();}bar();

回看上面的定義,該列印什麼?

value

讓我們分析下執行過程:
執行foo()函數,先從foo函數內部查詢是否有區域性變數value。如果沒有,就根據定義時的位置,查詢上面一層的程式碼,也就是value=1.所以結果會列印1。

這裡面當然不是如此簡單能概括的,你可以從執行上下文的角度分析一下。

建立作用域鏈

上面我們說了詞法環境(作用域)的兩個組成。再結合執行上下文,我們不難發現:通過外部詞法環境的參照,作用域可以順著棧層層拓展,建立起從當前環境向外延伸的一條鏈式結構。

再來看一個例子:

function foo(){
	console.dir(bar);
	var a=1;
	function bar(){
		a=2;
	}}console.dir(foo);foo();

由靜態作用域,全域性函數foo建立了一個自身物件的 [[scope]] 屬性

foo[[scope]]=[globalContext];

而當我們執行foo()時,也會先後進入foo函數的定義期和執行期。在foo函數的定義期時,函數bar的 [[scope]] 將會包含全域性內建scope和foo的內建scope

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

這證明了這一點:「JS會通過外部詞法環境參照來建立變數物件的一個作用域鏈,從而保證對執行環境有權存取的變數和函數的有序存取。」

讓我們再回頭看看執行上下文中的那道題,在前面我們說了它們有什麼不同,這裡說下為什麼它們相同地列印了「local scope」:還是那句話「JS採用的是詞法作用域,函數的作用域取決於函數建立的位置」 —— JS函數的執行用到了作用域鏈,這個作用域鏈是在函數定義的時候建立的。巢狀的函數 f() 定義在這個作用域鏈裡,其中的變數scope一定是指區域性變數,不管何時何地執行 f() ,這種繫結在執行 f() 時依然有效。

基於作用域鏈的變數查詢

當某個變數無法在自身詞法環境記錄中找到時,可以根據外部詞法環境參照向外層進行尋找,直到最外層的詞法環境中外部詞法環境參照為null
與此相似的是「物件中基於原型鏈的查詢」:

  • 原型:每一個JS物件(null 除外)在建立時就會與另一個物件關聯,這個物件就是我們說的原型。每一個物件都會從原型中「繼承」屬性。
  • 當讀取範例的屬性時,如果找不到,就會查詢與物件關聯的原型中的屬性,如果還找不到,就去找原型的原型,一直到最頂層(__proto__為null)為止

它們的區別也顯而易見:原型鏈是通過 prototype 屬性建立物件繼承的連結;而作用域鏈是指內部函數能存取到外部函數的閉包。不管直接還是間接,所有函數的作用域鏈最終都連結到全域性上下文。

相關推薦:

以上就是深入瞭解JavaScript引擎如何執行JS程式碼的詳細內容,更多請關注TW511.COM其它相關文章!