這是《圖解 Google V8》第一篇/共三篇:設計思想篇
這個專欄的優點是:寫的通俗易懂,沒有涉及 V8
原始碼部分,對於前端還是比較友好的,學完之後能夠知道寫下一段 js
程式碼後,V8
背後都做了哪些事情
這個專欄的不足之處是:沒有對技術進行深入講解,只講了這個技術是用來解決什麼問題的,以及它是怎麼工作的
所以這個專欄比較時候對 V8
還不瞭解的同學去學習,增加自己的知識面
下面是我自己學習每一章的總結,主要記錄我在這章中學到內容,並不是對這章完整的總結
如何學習谷歌高效能 JavaScript 引擎 V8?
V8
主要涉及三個技術:編譯流水線、事件迴圈系統、垃圾回收機制
V8
執行JavaScript
完整流程稱為:編譯流水線
編譯流水線涉及到的技術有:
JIT
V8
混合編譯執行和解釋執行
惰性解析
- 加快程式碼啟動速度
隱藏類(
Hide Class
)- 將動態型別轉為靜態型別,消除動態型別執行速度慢的問題
- 內聯快取
事件迴圈系統
JavaScript
中的難點:非同步程式設計- 排程排隊任務,讓
JavaScript
有序的執行
垃圾回收機制
- 記憶體分配
- 記憶體回收
01 | V8 是如何執行一段 JavaScript 程式碼的?
準備基礎環境:
- 全域性執行上下文:全域性作用、全域性變數、內建函式
- 初始化記憶體中的堆和棧結構
- 初始化訊息迴圈系統:訊息驅動器和訊息佇列
結構化
JavaScript
原始碼- 生成抽象語法樹(
AST
) - 生成相關作用域
- 生成抽象語法樹(
生成位元組碼:位元組碼是介於
AST
和機器碼的中間程式碼- 直譯器可以直接執行
- 編譯器需要將其編譯為二進位制的機器碼再執行
直譯器和監控機器人
- 直譯器:按照順序執行位元組碼,並輸出執行結果
- 監控機器人:如果發現某段程式碼被重複多次執行,將其標記為熱點程式碼
最佳化熱點程式碼
- 最佳化編譯器將熱點程式碼編譯為機器碼
- 對編譯後的機器碼進行最佳化
- 再次執行到這段程式碼時,將優先執行最佳化後的程式碼
反最佳化
JavaScript
物件在執行時可能會被改變,這段最佳化後的熱點程式碼就失效了- 進行反最佳化操作,給到直譯器解釋執行
02 | 函式即物件:一篇文章徹底搞懂 JavaScript 的函式特點
V8
內部為函式物件新增了兩個隱藏屬性:name
,code
:
name
為函式名- 如果是匿名函式,
name
為anonymous
- 如果是匿名函式,
code
為函式程式碼,以字串的形式儲存在記憶體中
當執行到一個函式呼叫語句時,V8
從函式物件中取出 code
屬性值,然後解釋執行這段函式程式碼
什麼是閉包:將外部變數和函式繫結起來的技術
參考資料:
03 | 快屬性和慢屬性:V8 是怎樣提升物件屬性訪問速度的?
V8
在實現物件儲存時,沒有完全採用字典的儲存方式,因為字典是非線性的資料結構,查詢效率會低於線性的資料結構
常規屬性和索引屬性
- 索引屬性(
elements
):數字屬性按照索引值的大小升序排列 - 常規屬性(
properties
):字串根據建立時的順序升序排列
它們都是線性資料結構,分別為 elements
物件和 properties
物件
執行一次查詢:先從 elements
物件中按照順序讀出所有元素,然後再從 properties
物件中讀取所有的元素
快屬性和慢屬性
在訪問一個屬性時,比如:foo.a
,V8
先查詢出 properties
,然後在從 properties
中查詢出 a
屬性
V8
為了簡化這一步操作,把部分 properties
儲存到物件本身,預設是 10
個,這個被稱為物件內屬性
線性資料結構通常被稱為快屬性
線性資料結構進行大量資料的新增和刪除,執行效率是非常低的,所以 V8
會採用慢屬性策略
慢屬性的物件內部有獨立的非線性資料結構(字典)
參考資料:
04 | 函式表示式:涉及大量概念,函式表示式到底該怎麼學?
變數提升
在 js
中有函式宣告的方式有兩種:
函式宣告
function foo() { console.log("foo"); }
函式表示式
var foo = function () { console.log("foo"); };
在編譯階段 V8
解析到函式宣告和函式表示式(變數宣告)時:
- 函式宣告,將其轉換為記憶體中的函式物件,並放到作用域中
- 變數宣告,將其值設定為
undefined
,並當道作用域中
因為在編譯階段,是不會執行表示式的,只會分析變數的定義、函式的宣告
所以如果在宣告前呼叫 foo
函式:
- 使用函式宣告不會報錯
- 使用函式表示式會報錯
在編譯階段將所有的變數提升到作用域的過程稱為變數提升
立即執行函式
js
的圓括號 ()
可以在中間放一個表示式
中間如果是一個函式宣告,V8
就會把 (function(){})
看成是函式表示式,執行它會返回一個函式物件
如果在函式表示式後面加上()
,就被稱為立即呼叫函式表示式
因為函式立即表示式也是表示式,所以不會建立函式物件,就不會汙染環境
05 |原型鏈:V8 是如何實現物件繼承的?
- 作用域鏈是沿著函式的作用域一級一級來查詢變數的
- 原型鏈是沿著物件的原型一級一級來查詢屬性的
js
中實現繼承,是將 __proto__
指向物件,但是不推薦使用,主要原因是:
- 這是隱藏屬性,不是標準定義的
- 使用該屬性會造成嚴重的效能問題
繼承
用建構函式實現繼承:
function DogFactory(color) { this.color = color; } DogFactory.prototype.type = "dog"; const dog = new DogFactory("Black"); dog.hasOwnProperty("type"); // false
ES6
之後可以透過Object.create
實現繼承const animalType = { type: "dog" }; const dog = Object.create(animalType); dog.hasOwnProperty("type"); // false
new 背後做了這些事情
- 幫你在內部建立一個臨時物件
- 將臨時物件的
__proto__
設定為建構函式的原型,建構函式的原型統一叫做prototype
return
臨時物件
function NEW(fn) {
return function () {
var o = { __proto__: fn.prototype };
fn.apply(o, arguments);
return o;
};
}
__proto__
、prototype
、constructor
區別
prototype
是函式的獨有的;__proto__
和 constructor
是物件獨有的
由於函式也是物件,所以函式也有 __proto__
和 constructor
constructor
是函式;prototype
和 __proto__
是物件
typeof Object.__proto__; // "object"
typeof Object.prototype; // "object"
typeof Object.constructor; // "function"
let obj = new Object();
obj.__proto__ === Object.prototype;
obj.constructor === Object;
obj
是 Object
的例項,所以 obj.constructor === Object
obj
的是物件,Object
是函式,所以 obj.__proto__ === Object.prototype
參考資料:
06 |作用域鏈:V8 是如何查詢變數的?
全域性作用域是在 V8
啟動過程中就建立了,且一直儲存在記憶體中不會被銷燬的,直至 V8
退出
而函式作用域是在執行該函式時建立的,當函式執行結束之後,函式作用域就隨之被銷燬掉了
因為 JavaScript
是基於詞法作用域的,詞法作用域就是指,查詢作用域的順序是按照函式定義時的位置來決定的。
詞法作用域是靜態作用域,根據函式在程式碼中的位置來確定的,作用域是在宣告函式時就確定好了
動態作用域鏈是基於呼叫棧的,不是基於函式定義的位置的,可以認為 this
是用來彌補 JavaScript
沒有動態作用域特性的
07 |型別轉換:V8 是怎麼實現 1+“2”的?
V8
會提供了一個 ToPrimitive
方法,其作用是將 a
和 b
轉換為原生資料型別
- 先檢測該物件中是否存在
valueOf
方法,如果有並返回了原始型別,那麼就使用該值進行強制型別轉換 - 如果
valueOf
沒有返回原始型別,那麼就使用toString
方法的返回值 - 如果
valueOf
和toString
兩個方法都不返回基本型別值,便會觸發一個TypeError
的錯誤。