我們前幾章和講解了什麼瀏覽器的組成部分以及渲染引擎,今天我們主要講一下js引擎的相關知識點,那麼在開講之前我們需要回顧一下有關渲染引擎的相關知識點
渲染引擎
關鍵渲染路徑是指瀏覽器從最初接收請求來的HTML、CSS、javascript等資源,然後解析、構建樹、渲染布局、繪製,最後呈現給客戶能看到的界面這整個過程。
JavaScript引擎
JavaScript引擎是一個專門處理JavaScript腳本的虛擬機,一般會附帶在網頁瀏覽器中。JavaScript引擎從頭到尾負責整個JavaScript程序的編譯和執行過程。js的引擎有很多種,而最爲大家熟知的無疑是V8引擎,他用於Chrome瀏覽器和Node中。
V8引擎由兩個主要部件組成:
emory Heap(內存堆) — 內存分配地址的地方
Call Stack(調用堆棧) — 代碼執行的地方
上面只是簡單的對js引擎進行一下基本的瞭解,下面開始正式介紹js引擎的執行過程(以V8引擎爲例)
js引擎執行過程
全面分析js引擎的執行過程,主要分爲三個階段:
1. 語法分析
2. 預編譯階段
3. 執行階段
下面着重講一下這三個階段:
語法分析
分析該js腳本代碼塊的語法是否正確,如果出現不正確,則向外拋出一個語法錯誤(SyntaxError),停止該js代碼塊的執行,然後繼續查找並加載下一個代碼塊;如果語法正確,則進入預編譯階段
預編譯階段
js代碼塊通過語法分析階段之後,語法都正確的下回進入預編譯階段。
在分析預編譯階段之前,我們先來了解一下js的運行環境,運行環境主要由三種:
1、全局環境(js代碼加載完畢後,進入到預編譯也就是進入到全局環境)
2、函數環境(函數調用的時候,進入到該函數環境,不同的函數,函數環境不同)
3、eval環境(不建議使用,存在安全、性能問題)
每進入到一個不同的運行環境都會創建一個相應的執行上下文(execution context)「下文會介紹」,那麼在一段js程序中一般都會創建多個執行上下文,js引擎會以棧的數據結構對這些執行進行處理,形成函數調用棧(call stack),棧底永遠是全局執行上下文(global execution context),棧頂則永遠時當前的執行上下文。
注意:執行上下文的相關概念會在下面進一步進行詳細介紹
執行階段
在執行階段,我們暫時先不考慮異步(因爲異步階段涉及到的知識點是事件循環【event loop】),等讀完這篇文章之後並且理解之後,去看我寫的深入理解事件循環這篇文章,就會進一步理解。
我們上文講到V8引擎由兩個主要部件組成:
emory Heap(內存堆) — 內存分配地址的地方
Call Stack(調用堆棧)— 代碼執行的地方
我們聲明的函數與變量被儲存在『內存堆』
中,而當我們要執行的時候,就必須藉助於『調用棧』
來解決問題。函數調用棧就是使用棧存取的方式進行管理運行環境,特點是先進後出,後進後出
我們來分析一下js代碼來理解函數調用棧:
function bar() {
var B_context = "bar saucxs";
function foo() {
var f_context = "foo saucxs";
}
foo()
}
bar()
上面代碼塊通過語法分析後,進入預編譯階段創建執行上下文,如圖所示
1、首先進入到全局環境,創建全局執行上下文(global Execution Context ),推入到stack中;
2、調用bar函數,進入bar函數運行環境,創建bar函數執行上下文(bar Execution Context),推入stack棧中;
3、在bar函數內部調用foo函數,則再進入到foo函數運行環境中,創建foo函數執行上下文(foo Execution Context),如上圖,由於foo函數內部沒有再調用其他函數,那麼則開始出棧;
5、foo函數執行完畢之後,棧頂foo函數執行上下文(foo Execution Context)首先出棧;
6、bar函數執行完畢,bar函數執行上下文(bar Execution Context)出棧;
7、全局上下文(global Execution Cntext)在瀏覽器或者該標籤關閉的時候出棧。
說明:不同的運行環境執行都會進入到代碼預編譯和執行兩個階段,語法分析則在代碼塊加載完畢時統一檢查語法。
上面的就是我們簡單的對一段代碼進行分析的過程,下面,我們講一下在預編譯階段提到的執行上下文
執行上下文
執行上下文可理解爲當前的執行環境,與該運行環境相對應.前面我們提到過,JavaScript中有三種可執行代碼塊,當然也對應着三種執行上下文。
全局執行上下文
這是基礎上下文,任何不在函數內部的代碼都在全局上下文中。一個程序中只會有一個全局執行上下文。它會執行兩件事:創建一個全局的 window 對象(瀏覽器的情況下),並且設置 this 的值等於這個全局對象。函數執行上下文
每當一個函數被調用時, 都會爲該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建。Eval 執行上下文
執行在 eval 內部的代碼也會有它屬於自己的執行上下文,除非你想搞黑魔法,不然不要輕易使用它。
執行上下文分爲兩個階段:
- 創建階段
- 執行階段
我們主要討論創建階段,執行階段的主要工作就是分配變量
創建階段
創建執行上下文的過程中,主要是做了下面三件事,如圖所示:
1、確定 this 的值,也被稱爲 This Binding。
2、LexicalEnvironment(詞法環境) 組件被創建。
3、VariableEnvironment(變量環境) 組件被創建。
This Binding
全局執行上下文中,
this
的值指向全局對象,在瀏覽器中this
的值指向window
對象,而在nodejs
中指向這個文件的module
對象。函數執行上下文中,
this
的值取決於函數的調用方式。具體有:默認綁定、隱式綁定、顯式綁定(硬綁定)、new
綁定、箭頭函數,具體內容請參考JavaScript深入之史上最全--5種this綁定全面解析這篇文章
。
詞法環境有兩個組成部分
1、環境記錄:存儲變量和函數聲明的實際位置
2、對外部環境的引用:可以訪問其外部詞法環境
詞法環境有兩種類型
1、全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,
this
的值指向這個全局對象。2、函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了
arguments
對象。對外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境。
直接看僞代碼可能更加直觀
GlobalExectionContext = { // 全局執行上下文
LexicalEnvironment: { // 詞法環境
EnvironmentRecord: { // 環境記錄
Type: "Object", // 全局環境
// 標識符綁定在這裏
outer: <null> // 對外部環境的引用
}
}
FunctionExectionContext = { // 函數執行上下文
LexicalEnvironment: { // 詞法環境
EnvironmentRecord: { // 環境記錄
Type: "Declarative", // 函數環境
// 標識符綁定在這裏 // 對外部環境的引用
outer: <Global or outer function environment reference>
}
}
變量環境
變量環境也是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性。
在 ES6 中,詞法 環境和 變量 環境的區別在於前者用於存儲函數聲明和變量( let
和 const
)綁定,而後者僅用於存儲變量( var
)綁定。
使用例子進行介紹
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
執行上下文如下所示
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這裏
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這裏
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這裏
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這裏
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
變量提升的原因:在創建階段,函數聲明存儲在環境中,而變量會被設置爲 undefined
(在 var
的情況下)或保持未初始化(在 let
和 const
的情況下)。所以這就是爲什麼可以在聲明之前訪問 var
定義的變量(儘管是 undefined
),但如果在聲明之前訪問 let
和 const
定義的變量就會提示引用錯誤的原因。這就是所謂的變量提升。
執行上下文的屬性
對於每個執行上下文,都有三個重要屬性:
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
下面我們逐一簡略講解下:
變量對象
變量對象是與執行上下文相關的數據作用域,存儲了在上下文中定義的變量和函數聲明。
不同執行上下文下的變量對象稍有不同
全局上下文中的變量對象是全局對象
函數上下文:
在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。
活動對象和變量對象其實是一個東西,但是兩者的區別在於
- 1、變量對象(VO)是規範上或者是JS引擎上實現的,並不能在JS環境中直接訪問。
- 2、當進入到一個執行上下文後,這個變量對象纔會被激活,所以叫活動對象(AO),這時候活動對象上的各種屬性才能被訪問。
活動對象是在進入函數上下文時被創建的,它通過函數的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。
函數上下文的執行過程
執行上下文的代碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:
- 進入執行上下文
- 代碼執行
進入執行上下文
當進入執行上下文時,這時候還沒有執行代碼,變量對象會包括:
- 1、函數的所有形參 (如果是函數上下文):由名稱和對應值組成的一個變量對象的屬性被創建,如果沒有實參,屬性值設爲undefined。
- 2、函數聲明:由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建,如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性。
- 3、變量聲明:由名稱和對應值(undefined)組成一個變量對象的屬性被創建,如果變量名稱跟已經聲明的形參或函數相同,則變量聲明不會干擾已經存在的這類屬性。
代碼執行
在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值
舉個例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在進入執行上下文後,這時候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
代碼執行的時候變成:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
下面思考這兩道題目:輸出的會是什麼?
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
然而去看這段代碼:
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
這是因爲在進入執行上下文時,首先會處理函數聲明,其次會處理變量聲明,如果如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。
我們上文講了每個執行上下文都有三個重要的屬性,變量對象,作用域和this,由於作用域和this的篇幅過於長,所以,暫時不在這篇文章做展示
參考文檔
https://www.cxymsg.com/guide/mechanism.html#javascript%E7%9A%84%E6%89%A7%E8%A1%8C%E7%8E%AF%E5%A2%83
https://segmentfault.com/a/1190000017812175
https://juejin.im/post/5dde27615188256ebd1618fb
https://muyiy.cn/blog/1/1.1.html
https://github.com/mqyqingfeng/Blog/issues/5