您好,登錄后才能下訂單哦!
由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法,相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。
V8 是由 Google 開(kāi)發(fā)的開(kāi)源 JavaScript 引擎,也被稱為虛擬機(jī),模擬實(shí)際計(jì)算機(jī)各種功能來(lái)實(shí)現(xiàn)代碼的編譯和執(zhí)行。
為什么需要 JavaScript 引擎
我們寫(xiě)的 JavaScript 代碼直接交給瀏覽器或者 Node 執(zhí)行時(shí),底層的 CPU 是不認(rèn)識(shí)的,也沒(méi)法執(zhí)行。CPU 只認(rèn)識(shí)自己的指令集,指令集對(duì)應(yīng)的是匯編代碼。寫(xiě)匯編代碼是一件很痛苦的事情。并且不同類型的 CPU 的指令集是不一樣的,那就意味著需要給每一種 CPU 重寫(xiě)匯編代碼。
JavaScirpt 引擎可以將 JS 代碼編譯為不同 CPU(Intel, ARM 以及 MIPS 等)對(duì)應(yīng)的匯編代碼,這樣我們就不需要去翻閱每個(gè) CPU 的指令集手冊(cè)來(lái)編寫(xiě)匯編代碼了。當(dāng)然,JavaScript 引擎的工作也不只是編譯代碼,它還要負(fù)責(zé)執(zhí)行代碼、分配內(nèi)存以及垃圾回收。
1000100111011000 #機(jī)器指令 mov ax,bx #匯編指令
資料拓展: 匯編語(yǔ)言入門教程【阮一峰】 | 理解 V8 的字節(jié)碼「譯」
https://zhuanlan.zhihu.com/p/28590489
熱門 JavaScript 引擎
V8 (Google),用 C++編寫(xiě),開(kāi)放源代碼,由 Google 丹麥開(kāi)發(fā),是 Google Chrome 的一部分,也用于 Node.js。
JavaScriptCore (Apple),開(kāi)放源代碼,用于 webkit 型瀏覽器,如 Safari ,2008 年實(shí)現(xiàn)了編譯器和字節(jié)碼解釋器,升級(jí)為了 SquirrelFish。蘋(píng)果內(nèi)部代號(hào)為“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
Rhino,由 Mozilla 基金會(huì)管理,開(kāi)放源代碼,完全以 Java 編寫(xiě),用于 HTMLUnit
SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,現(xiàn)時(shí)用于 Mozilla Firefox。
Chakra (JScript 引擎),用于 Internet Explorer。
Chakra (JavaScript 引擎),用于 Microsoft Edge。
KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波頓開(kāi)發(fā),用于 KDE 項(xiàng)目的 Konqueror 網(wǎng)頁(yè)瀏覽器中。
JerryScript — 三星推出的適用于嵌入式設(shè)備的小型 JavaScript 引擎。
其他:Nashorn、QuickJS 、 Hermes
V8
Google V8 引擎是用 C ++編寫(xiě)的開(kāi)源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以運(yùn)行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 處理器的 Linux 系統(tǒng)上。V8 最早被開(kāi)發(fā)用以嵌入到 Google 的開(kāi)源瀏覽器 Chrome 中,第一個(gè)版本隨著第一版Chrome于 2008 年 9 月 2 日發(fā)布。但是 V8 是一個(gè)可以獨(dú)立運(yùn)行的模塊,完全可以嵌入到任何 C ++應(yīng)用程序中。著名的 Node.js( 一個(gè)異步的服務(wù)器框架,可以在服務(wù)端使用 JavaScript 寫(xiě)出高效的網(wǎng)絡(luò)服務(wù)器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。  
和其他 JavaScript 引擎一樣,V8 會(huì)編譯 / 執(zhí)行 JavaScript 代碼,管理內(nèi)存,負(fù)責(zé)垃圾回收,與宿主語(yǔ)言的交互等。通過(guò)暴露宿主對(duì)象 ( 變量,函數(shù)等 ) 到 JavaScript,JavaScript 可以訪問(wèn)宿主環(huán)境中的對(duì)象,并在腳本中完成對(duì)宿主對(duì)象的操作。
什么是 D8
d8 是一個(gè)非常有用的調(diào)試工具,你可以把它看成是 debug for V8 的縮寫(xiě)。我們可以使用 d8 來(lái)查看 V8 在執(zhí)行 JavaScript 過(guò)程中的各種中間數(shù)據(jù),比如作用域、AST、字節(jié)碼、優(yōu)化的二進(jìn)制代碼、垃圾回收的狀態(tài),還可以使用 d8 提供的私有 API 查看一些內(nèi)部信息。
安裝 D8
方法一:自行下載編譯
v8 google 下載及編譯使用
官方文檔:Using d8
方法二:使用編譯好的 d8 工具
mac 平臺(tái):
https://storage.googleapis.com/chromium-v8/official/canary/v8-mac64-dbg-8.4.109.zip
linux32 平臺(tái):
https://storage.googleapis.com/chromium-v8/official/canary/v8-linux32-dbg-8.4.109.zip
linux64 平臺(tái):
https://storage.googleapis.com/chromium-v8/official/canary/v8-linux64-dbg-8.4.109.zip
win32 平臺(tái):
https://storage.googleapis.com/chromium-v8/official/canary/v8-win32-dbg-8.4.109.zip
win64 平臺(tái):
https://storage.googleapis.com/chromium-v8/official/canary/v8-win64-dbg-8.4.109.zip
// 解壓文件,點(diǎn)擊d8打開(kāi)(mac安全策略限制的話,按住control,再點(diǎn)擊,彈出菜單中選擇打開(kāi)) V8 version 8.4.109 d8> 1 + 2 3 d8> 2 + '4' "24" d8> console.log(23) 23 undefined d8> var a = 1 undefined d8> a + 2 3 d8> this [object global] d8>
本文后續(xù)用于 demo 演示時(shí)的文件目錄結(jié)構(gòu):
V8: # d8可執(zhí)行文件 d8 icudtl.dat libc++.dylib libchrome_zlib.dylib libicui18n.dylib libicuuc.dylib libv8.dylib libv8_debug_helper.dylib libv8_for_testing.dylib libv8_libbase.dylib libv8_libplatform.dylib obj snapshot_blob.bin v8_build_config.json # 新建的js示例文件 test.js
方法三:mac
# 如果已有HomeBrew,忽略第一條命令 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install v8
方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解釋器)生成的 Bytecode(字節(jié)碼)。
都有哪些 d8 命令可供使用?
查看 d8 命令
# 如果不想使用./d8這種方式進(jìn)行調(diào)試,可將d8加入環(huán)境變量,之后就可以直接`d8 --help`了 ./d8 --help
過(guò)濾特定的命令,如:
# 如果是 Windows 系統(tǒng),可能缺少 grep 程序,請(qǐng)自行下載安裝并添加環(huán)境變量 ./d8 --help |grep print
print-bytecode 查看生成的字節(jié)碼
print-opt-code 查看優(yōu)化后的代碼
print-ast 查看中間生成的 AST
print-scopes 查看中間生成的作用域
trace-gc 查看這段代碼的內(nèi)存回收狀態(tài)
trace-opt 查看哪些代碼被優(yōu)化了
trace-deopt 查看哪些代碼被反優(yōu)化了
turbofan-stats 打印優(yōu)化編譯器的一些統(tǒng)計(jì)數(shù)據(jù)
使用 d8 進(jìn)行調(diào)試
// test.js function sum(a) { var b = 6; return a + 6; } console.log(sum(3));
# d8 后面跟上文件名和要執(zhí)行的命令,如執(zhí)行下面這行命令,就會(huì)打印出 test.js 文件所生成的字節(jié)碼。 ./d8 ./test.js --print-bytecode # 執(zhí)行以下命令,輸出9 ./d8 ./test.js
內(nèi)部方法
你還可以使用 V8 所提供的一些內(nèi)部方法,只需要在啟動(dòng) V8 時(shí)傳入 --allow-natives-syntax 命令,你就可以在 test.js 中使用諸如HasFastProperties(檢查一個(gè)對(duì)象是否擁有快屬性)的內(nèi)部方法(索引屬性、常規(guī)屬性、快屬性等下文會(huì)介紹)。
function Foo(property_num, element_num) { //添加可索引屬性 for (let i = 0; i < element_num; i++) { this[i] = `element${i}`; } //添加常規(guī)屬性 for (let i = 0; i < property_num; i++) { let ppt = `property${i}`; this[ppt] = ppt; } } var bar = new Foo(10, 10); // 檢查一個(gè)對(duì)象是否擁有快屬性 console.log(%HasFastProperties(bar)); delete bar.property2; console.log(%HasFastProperties(bar));
./d8 --allow-natives-syntax ./test.js # 依次打?。簍rue false
V8 引擎的內(nèi)部結(jié)構(gòu)
V8 是一個(gè)非常復(fù)雜的項(xiàng)目,有超過(guò) 100 萬(wàn)行 C++代碼。它由許多子模塊構(gòu)成,其中這 4 個(gè)模塊是最重要的:
Parser:負(fù)責(zé)將 JavaScript 源碼轉(zhuǎn)換為 Abstract Syntax Tree (AST)
Ignition:interpreter,即解釋器,負(fù)責(zé)將 AST 轉(zhuǎn)換為 Bytecode,解釋執(zhí)行 Bytecode;同時(shí)收集 TurboFan 優(yōu)化編譯所需的信息,比如函數(shù)參數(shù)的類型;解釋器執(zhí)行時(shí)主要有四個(gè)模塊,內(nèi)存中的字節(jié)碼、寄存器、棧、堆。
通常有兩種類型的解釋器,基于棧 (Stack-based)和基于寄存器 (Register-based),基于棧的解釋器使用棧來(lái)保存函數(shù)參數(shù)、中間運(yùn)算結(jié)果、變量等;基于寄存器的虛擬機(jī)則支持寄存器的指令操作,使用寄存器來(lái)保存參數(shù)、中間計(jì)算結(jié)果。通常,基于棧的虛擬機(jī)也定義了少量的寄存器,基于寄存器的虛擬機(jī)也有堆棧,其區(qū)別體現(xiàn)在它們提供的指令集體系。大多數(shù)解釋器都是基于棧的,比如 Java 虛擬機(jī),.Net 虛擬機(jī),還有早期的 V8 虛擬機(jī)。基于堆棧的虛擬機(jī)在處理函數(shù)調(diào)用、解決遞歸問(wèn)題和切換上下文時(shí)簡(jiǎn)單明快。而現(xiàn)在的 V8 虛擬機(jī)則采用了基于寄存器的設(shè)計(jì),它將一些中間數(shù)據(jù)保存到寄存器中。
基于寄存器的解釋器架構(gòu):
TurboFan:compiler,即編譯器,利用 Ignitio 所收集的類型信息,將 Bytecode 轉(zhuǎn)換為優(yōu)化的匯編代碼;
Orinoco:garbage collector,垃圾回收模塊,負(fù)責(zé)將程序不再需要的內(nèi)存空間回收。
其中,Parser,Ignition 以及 TurboFan 可以將 JS 源碼編譯為匯編代碼,其流程圖如下:
  
簡(jiǎn)單地說(shuō),Parser 將 JS 源碼轉(zhuǎn)換為 AST,然后 Ignition 將 AST 轉(zhuǎn)換為 Bytecode,最后 TurboFan 將 Bytecode 轉(zhuǎn)換為經(jīng)過(guò)優(yōu)化的 Machine Code(實(shí)際上是匯編代碼)。
如果函數(shù)沒(méi)有被調(diào)用,則 V8 不會(huì)去編譯它。
如果函數(shù)只被調(diào)用 1 次,則 Ignition 將其編譯 Bytecode 就直接解釋執(zhí)行了。TurboFan 不會(huì)進(jìn)行優(yōu)化編譯,因?yàn)樗枰?Ignition 收集函數(shù)執(zhí)行時(shí)的類型信息。這就要求函數(shù)至少需要執(zhí)行 1 次,TurboFan 才有可能進(jìn)行優(yōu)化編譯。
如果函數(shù)被調(diào)用多次,則它有可能會(huì)被識(shí)別為熱點(diǎn)函數(shù),且 Ignition 收集的類型信息證明可以進(jìn)行優(yōu)化編譯的話,這時(shí) TurboFan 則會(huì)將 Bytecode 編譯為 Optimized Machine Code(已優(yōu)化的機(jī)器碼),以提高代碼的執(zhí)行性能。  
圖片中的紅色虛線是逆向的,也就是說(shuō)Optimized Machine Code 會(huì)被還原為 Bytecode,這個(gè)過(guò)程叫做 Deoptimization。這是因?yàn)?Ignition 收集的信息可能是錯(cuò)誤的,比如 add 函數(shù)的參數(shù)之前是整數(shù),后來(lái)又變成了字符串。生成的 Optimized Machine Code 已經(jīng)假定 add 函數(shù)的參數(shù)是整數(shù),那當(dāng)然是錯(cuò)誤的,于是需要進(jìn)行 Deoptimization。
function add(x, y) { return x + y; } add(3, 5); add('3', '5');
在運(yùn)行 C、C++以及 Java 等程序之前,需要進(jìn)行編譯,不能直接執(zhí)行源碼;但對(duì)于 JavaScript 來(lái)說(shuō),我們可以直接執(zhí)行源碼(比如:node test.js),它是在運(yùn)行的時(shí)候先編譯再執(zhí)行,這種方式被稱為即時(shí)編譯(Just-in-time compilation),簡(jiǎn)稱為 JIT。因此,V8 也屬于 JIT 編譯器。
V8 是怎么執(zhí)行一段 JavaScript 代碼的
在 V8 出現(xiàn)之前,所有的 JavaScript 虛擬機(jī)所采用的都是解釋執(zhí)行的方式,這是 JavaScript 執(zhí)行速度過(guò)慢的一個(gè)主要原因。而 V8 率先引入了即時(shí)編譯(JIT)的雙輪驅(qū)動(dòng)的設(shè)計(jì)(混合使用編譯器和解釋器的技術(shù)),這是一種權(quán)衡策略,混合編譯執(zhí)行和解釋執(zhí)行這兩種手段,給 JavaScript 的執(zhí)行速度帶來(lái)了極大的提升。V8 出現(xiàn)之后,各大廠商也都在自己的 JavaScript 虛擬機(jī)中引入了 JIT 機(jī)制,所以目前市面上 JavaScript 虛擬機(jī)都有著類似的架構(gòu)。另外,V8 也是早于其他虛擬機(jī)引入了惰性編譯、內(nèi)聯(lián)緩存、隱藏類等機(jī)制,進(jìn)一步優(yōu)化了 JavaScript 代碼的編譯執(zhí)行效率。
V8 執(zhí)行一段 JavaScript 的流程圖:
V8 本質(zhì)上是一個(gè)虛擬機(jī),因?yàn)橛?jì)算機(jī)只能識(shí)別二進(jìn)制指令,所以要讓計(jì)算機(jī)執(zhí)行一段高級(jí)語(yǔ)言通常有兩種手段:
第一種是將高級(jí)代碼轉(zhuǎn)換為二進(jìn)制代碼,再讓計(jì)算機(jī)去執(zhí)行;
另外一種方式是在計(jì)算機(jī)安裝一個(gè)解釋器,并由解釋器來(lái)解釋執(zhí)行。
解釋執(zhí)行和編譯執(zhí)行都有各自的優(yōu)缺點(diǎn),解釋執(zhí)行啟動(dòng)速度快,但是執(zhí)行時(shí)速度慢,而編譯執(zhí)行啟動(dòng)速度慢,但是執(zhí)行速度快。為了充分地利用解釋執(zhí)行和編譯執(zhí)行的優(yōu)點(diǎn),規(guī)避其缺點(diǎn),V8 采用了一種權(quán)衡策略,在啟動(dòng)過(guò)程中采用了解釋執(zhí)行的策略,但是如果某段代碼的執(zhí)行頻率超過(guò)一個(gè)值,那么 V8 就會(huì)采用優(yōu)化編譯器將其編譯成執(zhí)行效率更加高效的機(jī)器代碼。
總結(jié):
V8 執(zhí)行一段 JavaScript 代碼所經(jīng)歷的主要流程包括:
初始化基礎(chǔ)環(huán)境;
解析源碼生成 AST 和作用域;
依據(jù) AST 和作用域生成字節(jié)碼;
解釋執(zhí)行字節(jié)碼;
監(jiān)聽(tīng)熱點(diǎn)代碼;
優(yōu)化熱點(diǎn)代碼為二進(jìn)制的機(jī)器代碼;
反優(yōu)化生成的二進(jìn)制機(jī)器代碼。
一等公民與閉包
一等公民的定義
在編程語(yǔ)言中,一等公民可以作為函數(shù)參數(shù),可以作為函數(shù)返回值,也可以賦值給變量。
如果某個(gè)編程語(yǔ)言的函數(shù),可以和這個(gè)語(yǔ)言的數(shù)據(jù)類型做一樣的事情,我們就把這個(gè)語(yǔ)言中的函數(shù)稱為一等公民。例如,字符串在幾乎所有編程語(yǔ)言中都是一等公民,字符串可以做為函數(shù)參數(shù),字符串可以作為函數(shù)返回值,字符串也可以賦值給變量。對(duì)于各種編程語(yǔ)言來(lái)說(shuō),函數(shù)就不一定是一等公民了,比如 Java 8 之前的版本。
對(duì)于 JavaScript 來(lái)說(shuō),函數(shù)可以賦值給變量,也可以作為函數(shù)參數(shù),還可以作為函數(shù)返回值,因此 JavaScript 中函數(shù)是一等公民。
動(dòng)態(tài)作用域與靜態(tài)作用域
如果一門語(yǔ)言的作用域是靜態(tài)作用域,那么符號(hào)之間的引用關(guān)系能夠根據(jù)程序代碼在編譯時(shí)就確定清楚,在運(yùn)行時(shí)不會(huì)變。某個(gè)函數(shù)是在哪聲明的,就具有它所在位置的作用域。它能夠訪問(wèn)哪些變量,那么就跟這些變量綁定了,在運(yùn)行時(shí)就一直能訪問(wèn)這些變量。即靜態(tài)作用域可以由程序代碼決定,在編譯時(shí)就能完全確定。大多數(shù)語(yǔ)言都是靜態(tài)作用域的。
動(dòng)態(tài)作用域(Dynamic Scope)。也就是說(shuō),變量引用跟變量聲明不是在編譯時(shí)就綁定死了的。在運(yùn)行時(shí),它是在運(yùn)行環(huán)境中動(dòng)態(tài)地找一個(gè)相同名稱的變量。在 macOS 或 Linux 中用的 bash 腳本語(yǔ)言,就是動(dòng)態(tài)作用域的。
閉包的三個(gè)基礎(chǔ)特性
JavaScript 語(yǔ)言允許在函數(shù)內(nèi)部定義新的函數(shù)
可以在內(nèi)部函數(shù)中訪問(wèn)父函數(shù)中定義的變量
因?yàn)?JavaScript 中的函數(shù)是一等公民,所以函數(shù)可以作為另外一個(gè)函數(shù)的返回值
// 閉包(靜態(tài)作用域,一等公民,調(diào)用棧的矛盾體) function foo() { var d = 20; return function inner(a, b) { const c = a + b + d; return c; }; } const f = foo();
關(guān)于閉包,可參考我以前的一篇文章,在此不再贅述,在此主要談下閉包給 Chrome V8 帶來(lái)的問(wèn)題及其解決策略。
惰性解析  
所謂惰性解析是指解析器在解析的過(guò)程中,如果遇到函數(shù)聲明,那么會(huì)跳過(guò)函數(shù)內(nèi)部的代碼,并不會(huì)為其生成 AST 和字節(jié)碼,而僅僅生成頂層代碼的 AST 和字節(jié)碼。
在編譯 JavaScript 代碼的過(guò)程中,V8 并不會(huì)一次性將所有的 JavaScript 解析為中間代碼,這主要是基于以下兩點(diǎn):
首先,如果一次解析和編譯所有的 JavaScript 代碼,過(guò)多的代碼會(huì)增加編譯時(shí)間,這會(huì)嚴(yán)重影響到首次執(zhí)行 JavaScript 代碼的速度,讓用戶感覺(jué)到卡頓。因?yàn)橛袝r(shí)候一個(gè)頁(yè)面的 JavaScript 代碼很大,如果要將所有的代碼一次性解析編譯完成,那么會(huì)大大增加用戶的等待時(shí)間;
其次,解析完成的字節(jié)碼和編譯之后的機(jī)器代碼都會(huì)存放在內(nèi)存中,如果一次性解析和編譯所有 JavaScript 代碼,那么這些中間代碼和機(jī)器代碼將會(huì)一直占用內(nèi)存。
基于以上的原因,所有主流的 JavaScript 虛擬機(jī)都實(shí)現(xiàn)了惰性解析。
閉包給惰性解析帶來(lái)的問(wèn)題:上文的 d 不能隨著 foo 函數(shù)的執(zhí)行上下文被銷毀掉。
預(yù)解析器
V8 引入預(yù)解析器,比如當(dāng)解析頂層代碼的時(shí)候,遇到了一個(gè)函數(shù),那么預(yù)解析器并不會(huì)直接跳過(guò)該函數(shù),而是對(duì)該函數(shù)做一次快速的預(yù)解析。
判斷當(dāng)前函數(shù)是不是存在一些語(yǔ)法上的錯(cuò)誤,發(fā)現(xiàn)了語(yǔ)法錯(cuò)誤,那么就會(huì)向 V8 拋出語(yǔ)法錯(cuò)誤;
檢查函數(shù)內(nèi)部是否引用了外部變量,如果引用了外部的變量,預(yù)解析器會(huì)將棧中的變量復(fù)制到堆中,在下次執(zhí)行到該函數(shù)的時(shí)候,直接使用堆中的引用,這樣就解決了閉包所帶來(lái)的問(wèn)題。
V8 內(nèi)部是如何存儲(chǔ)對(duì)象的:快屬性和慢屬性
下面的代碼會(huì)輸出什么:
// test.js function Foo() { this[200] = 'test-200'; this[1] = 'test-1'; this[100] = 'test-100'; this['B'] = 'bar-B'; this[50] = 'test-50'; this[9] = 'test-9'; this[8] = 'test-8'; this[3] = 'test-3'; this[5] = 'test-5'; this['D'] = 'bar-D'; this['C'] = 'bar-C'; } var bar = new Foo(); for (key in bar) { console.log(`index:${key} value:${bar[key]}`); } //輸出: // index:1 value:test-1 // index:3 value:test-3 // index:5 value:test-5 // index:8 value:test-8 // index:9 value:test-9 // index:50 value:test-50 // index:100 value:test-100 // index:200 value:test-200 // index:B value:bar-B // index:D value:bar-D // index:C value:bar-C
在ECMAScript 規(guī)范中定義了數(shù)字屬性應(yīng)該按照索引值大小升序排列,字符串屬性根據(jù)創(chuàng)建時(shí)的順序升序排列。在這里我們把對(duì)象中的數(shù)字屬性稱為排序?qū)傩?,?V8 中被稱為 elements,字符串屬性就被稱為常規(guī)屬性,在 V8 中被稱為 properties。在 V8 內(nèi)部,為了有效地提升存儲(chǔ)和訪問(wèn)這兩種屬性的性能,分別使用了兩個(gè)線性數(shù)據(jù)結(jié)構(gòu)來(lái)分別保存排序?qū)傩院统R?guī)屬性。同時(shí) v8 將部分常規(guī)屬性直接存儲(chǔ)到對(duì)象本身,我們把這稱為對(duì)象內(nèi)屬性 (in-object properties),不過(guò)對(duì)象內(nèi)屬性的數(shù)量是固定的,默認(rèn)是 10 個(gè)。
function Foo(property_num, element_num) { //添加可索引屬性 for (let i = 0; i < element_num; i++) { this[i] = `element${i}`; } //添加常規(guī)屬性 for (let i = 0; i < property_num; i++) { let ppt = `property${i}`; this[ppt] = ppt; } } var bar = new Foo(10, 10);
可以通過(guò) Chrome 開(kāi)發(fā)者工具的 Memory 標(biāo)簽,捕獲查看當(dāng)前的內(nèi)存快照。通過(guò)增大第一個(gè)參數(shù)來(lái)查看存儲(chǔ)變化。
我們將保存在線性數(shù)據(jù)結(jié)構(gòu)中的屬性稱之為“快屬性”,因?yàn)榫€性數(shù)據(jù)結(jié)構(gòu)中只需要通過(guò)索引即可以訪問(wèn)到屬性,雖然訪問(wèn)線性結(jié)構(gòu)的速度快,但是如果從線性結(jié)構(gòu)中添加或者刪除大量的屬性時(shí),則執(zhí)行效率會(huì)非常低,這主要因?yàn)闀?huì)產(chǎn)生大量時(shí)間和內(nèi)存開(kāi)銷。因此,如果一個(gè)對(duì)象的屬性過(guò)多時(shí),V8 就會(huì)采取另外一種存儲(chǔ)策略,那就是“慢屬性”策略,但慢屬性的對(duì)象內(nèi)部會(huì)有獨(dú)立的非線性數(shù)據(jù)結(jié)構(gòu) (字典) 作為屬性存儲(chǔ)容器。所有的屬性元信息不再是線性存儲(chǔ)的,而是直接保存在屬性字典中。
v8 屬性存儲(chǔ):
總結(jié):  
因?yàn)?JavaScript 中的對(duì)象是由一組組屬性和值組成的,所以最簡(jiǎn)單的方式是使用一個(gè)字典來(lái)保存屬性和值,但是由于字典是非線性結(jié)構(gòu),所以如果使用字典,讀取效率會(huì)大大降低。為了提升查找效率,V8 在對(duì)象中添加了兩個(gè)隱藏屬性,排序?qū)傩院统R?guī)屬性,element 屬性指向了 elements 對(duì)象,在 elements 對(duì)象中,會(huì)按照順序存放排序?qū)傩?。properties 屬性則指向了 properties 對(duì)象,在 properties 對(duì)象中,會(huì)按照創(chuàng)建時(shí)的順序保存常規(guī)屬性。  
通過(guò)引入這兩個(gè)屬性,加速了 V8 查找屬性的速度,為了更加進(jìn)一步提升查找效率,V8 還實(shí)現(xiàn)了內(nèi)置內(nèi)屬性的策略,當(dāng)常規(guī)屬性少于一定數(shù)量時(shí),V8 就會(huì)將這些常規(guī)屬性直接寫(xiě)進(jìn)對(duì)象中,這樣又節(jié)省了一個(gè)中間步驟。  
但是如果對(duì)象中的屬性過(guò)多時(shí),或者存在反復(fù)添加或者刪除屬性的操作,那么 V8 就會(huì)將線性的存儲(chǔ)模式降級(jí)為非線性的字典存儲(chǔ)模式,這樣雖然降低了查找速度,但是卻提升了修改對(duì)象的屬性的速度。
堆空間和??臻g
??臻g
現(xiàn)代語(yǔ)言都是基于函數(shù)的,每個(gè)函數(shù)在執(zhí)行過(guò)程中,都有自己的生命周期和作用域,當(dāng)函數(shù)執(zhí)行結(jié)束時(shí),其作用域也會(huì)被銷毀,因此,我們會(huì)使用棧這種數(shù)據(jù)結(jié)構(gòu)來(lái)管理函數(shù)的調(diào)用過(guò)程,我們也把管理函數(shù)調(diào)用過(guò)程的棧結(jié)構(gòu)稱之為調(diào)用棧。
??臻g主要是用來(lái)管理 JavaScript 函數(shù)調(diào)用的,棧是內(nèi)存中連續(xù)的一塊空間,同時(shí)棧結(jié)構(gòu)是“先進(jìn)后出”的策略。在函數(shù)調(diào)用過(guò)程中,涉及到上下文相關(guān)的內(nèi)容都會(huì)存放在棧上,比如原生類型、引用到的對(duì)象的地址、函數(shù)的執(zhí)行狀態(tài)、this 值等都會(huì)存在在棧上。當(dāng)一個(gè)函數(shù)執(zhí)行結(jié)束,那么該函數(shù)的執(zhí)行上下文便會(huì)被銷毀掉。
??臻g的最大的特點(diǎn)是空間連續(xù),所以在棧中每個(gè)元素的地址都是固定的,因此??臻g的查找效率非常高,但是通常在內(nèi)存中,很難分配到一塊很大的連續(xù)空間,因此,V8 對(duì)棧空間的大小做了限制,如果函數(shù)調(diào)用層過(guò)深,那么 V8 就有可能拋出棧溢出的錯(cuò)誤。
棧的優(yōu)勢(shì)和缺點(diǎn):
棧的結(jié)構(gòu)非常適合函數(shù)調(diào)用過(guò)程。
在棧上分配資源和銷毀資源的速度非常快,這主要?dú)w結(jié)于??臻g是連續(xù)的,分配空間和銷毀空間只需要移動(dòng)下指針就可以了。
雖然操作速度非常快,但是棧也是有缺點(diǎn)的,其中最大的缺點(diǎn)也是它的優(yōu)點(diǎn)所造成的,那就是棧是連續(xù)的,所以要想在內(nèi)存中分配一塊連續(xù)的大空間是非常難的,因此??臻g是有限的。
// 棧溢出 function factorial(n) { if (n === 1) { return 1; } return n * factorial(n - 1); } console.log(factorial(50000));
堆空間
堆空間是一種樹(shù)形的存儲(chǔ)結(jié)構(gòu),用來(lái)存儲(chǔ)對(duì)象類型的離散的數(shù)據(jù),JavaScript 中除了原生類型的數(shù)據(jù),其他的都是對(duì)象類型,諸如函數(shù)、數(shù)組,在瀏覽器中還有 window 對(duì)象、document 對(duì)象等,這些都是存在堆空間的。
宿主在啟動(dòng) V8 的過(guò)程中,會(huì)同時(shí)創(chuàng)建堆空間和??臻g,再繼續(xù)往下執(zhí)行,產(chǎn)生的新數(shù)據(jù)都會(huì)存放在這兩個(gè)空間中。
繼承
繼承就是一個(gè)對(duì)象可以訪問(wèn)另外一個(gè)對(duì)象中的屬性和方法,在 JavaScript 中,我們通過(guò)原型和原型鏈的方式來(lái)實(shí)現(xiàn)了繼承特性。
JavaScript 的每個(gè)對(duì)象都包含了一個(gè)隱藏屬性 __proto__ ,我們就把該隱藏屬性 __proto__ 稱之為該對(duì)象的原型 (prototype),__proto__ 指向了內(nèi)存中的另外一個(gè)對(duì)象,我們就把 __proto__ 指向的對(duì)象稱為該對(duì)象的原型對(duì)象,那么該對(duì)象就可以直接訪問(wèn)其原型對(duì)象的方法或者屬性。  
JavaScript 中的繼承非常簡(jiǎn)潔,就是每個(gè)對(duì)象都有一個(gè)原型屬性,該屬性指向了原型對(duì)象,查找屬性的時(shí)候,JavaScript 虛擬機(jī)會(huì)沿著原型一層一層向上查找,直至找到正確的屬性。
隱藏屬性__proto__
var animal = { type: 'Default', color: 'Default', getInfo: function () { return `Type is: ${this.type},color is ${this.color}.`; }, }; var dog = { type: 'Dog', color: 'Black', };
利用__proto__實(shí)現(xiàn)繼承:
dog.__proto__ = animal; dog.getInfo();
通常隱藏屬性是不能使用 JavaScript 來(lái)直接與之交互的。雖然現(xiàn)代瀏覽器都開(kāi)了一個(gè)口子,讓 JavaScript 可以訪問(wèn)隱藏屬性 __proto__,但是在實(shí)際項(xiàng)目中,我們不應(yīng)該直接通過(guò) __proto__ 來(lái)訪問(wèn)或者修改該屬性,其主要原因有兩個(gè):
首先,這是隱藏屬性,并不是標(biāo)準(zhǔn)定義的;
其次,使用該屬性會(huì)造成嚴(yán)重的性能問(wèn)題。因?yàn)?JavaScript 通過(guò)隱藏類優(yōu)化了很多原有的對(duì)象結(jié)構(gòu),所以通過(guò)直接修改__proto__會(huì)直接破壞現(xiàn)有已經(jīng)優(yōu)化的結(jié)構(gòu),觸發(fā) V8 重構(gòu)該對(duì)象的隱藏類!
構(gòu)造函數(shù)是怎么創(chuàng)建對(duì)象的? 
在 JavaScript 中,使用 new 加上構(gòu)造函數(shù)的這種組合來(lái)創(chuàng)建對(duì)象和實(shí)現(xiàn)對(duì)象的繼承。不過(guò)使用這種方式隱含的語(yǔ)義過(guò)于隱晦。其實(shí)是 JavaScript 為了吸引 Java 程序員、在語(yǔ)法層面去蹭 Java 熱點(diǎn),所以就被硬生生地強(qiáng)制加入了非常不協(xié)調(diào)的關(guān)鍵字 new。
function DogFactory(type, color) { this.type = type; this.color = color; } var dog = new DogFactory('Dog', 'Black');
其實(shí)當(dāng) V8 執(zhí)行上面這段代碼時(shí),V8 在背后悄悄地做了以下幾件事情:
var dog = {}; dog.__proto__ = DogFactory.prototype; DogFactory.call(dog, 'Dog', 'Black');
機(jī)器碼、字節(jié)碼
V8 為什么要引入字節(jié)碼
早期的 V8 為了提升代碼的執(zhí)行速度,直接將 JavaScript 源代碼編譯成了沒(méi)有優(yōu)化的二進(jìn)制機(jī)器代碼,如果某一段二進(jìn)制代碼執(zhí)行頻率過(guò)高,那么 V8 會(huì)將其標(biāo)記為熱點(diǎn)代碼,熱點(diǎn)代碼會(huì)被優(yōu)化編譯器優(yōu)化,優(yōu)化后的機(jī)器代碼執(zhí)行效率更高。
隨著移動(dòng)設(shè)備的普及,V8 團(tuán)隊(duì)逐漸發(fā)現(xiàn)將 JavaScript 源碼直接編譯成二進(jìn)制代碼存在兩個(gè)致命的問(wèn)題:
時(shí)間問(wèn)題:編譯時(shí)間過(guò)久,影響代碼啟動(dòng)速度;
空間問(wèn)題:緩存編譯后的二進(jìn)制代碼占用更多的內(nèi)存。
這兩個(gè)問(wèn)題無(wú)疑會(huì)阻礙 V8 在移動(dòng)設(shè)備上的普及,于是 V8 團(tuán)隊(duì)大規(guī)模重構(gòu)代碼,引入了中間的字節(jié)碼。字節(jié)碼的優(yōu)勢(shì)有如下三點(diǎn):
解決啟動(dòng)問(wèn)題:生成字節(jié)碼的時(shí)間很短;
解決空間問(wèn)題:字節(jié)碼雖然占用的空間比原始的 JavaScript 多,但是相較于機(jī)器代碼,字節(jié)碼還是小了太多,緩存字節(jié)碼會(huì)大大降低內(nèi)存的使用。
代碼架構(gòu)清晰:采用字節(jié)碼,可以簡(jiǎn)化程序的復(fù)雜度,使得 V8 移植到不同的 CPU 架構(gòu)平臺(tái)更加容易。
Bytecode 某種程度上就是匯編語(yǔ)言,只是它沒(méi)有對(duì)應(yīng)特定的 CPU,或者說(shuō)它對(duì)應(yīng)的是虛擬的 CPU。這樣的話,生成 Bytecode 時(shí)簡(jiǎn)單很多,無(wú)需為不同的 CPU 生產(chǎn)不同的代碼。要知道,V8 支持 9 種不同的 CPU,引入一個(gè)中間層 Bytecode,可以簡(jiǎn)化 V8 的編譯流程,提高可擴(kuò)展性。
如果我們?cè)诓煌布先ド?Bytecode,會(huì)發(fā)現(xiàn)生成代碼的指令是一樣的。
如何查看字節(jié)碼
// test.js function add(x, y) { var z = x + y; return z; } console.log(add(1, 2));
運(yùn)行./d8 ./test.js --print-bytecode:
[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)] Parameter count 3 #三個(gè)參數(shù),包括了顯式地傳入的 x 和 y,還有一個(gè)隱式地傳入的 this Register count 1 Frame size 8 0x10008250026 @ 0 : 25 02 Ldar a1 #將a1寄存器中的值加載到累加器中,LoaD Accumulator from Register 0x10008250028 @ 2 : 34 03 00 Add a0, [0] 0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中 0x1000825002d @ 7 : aa Return #結(jié)束當(dāng)前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方 Constant pool (size = 0) Handler Table (size = 0) Source Position Table (size = 0) 3
常用字節(jié)碼指令:
Ldar:表示將寄存器中的值加載到累加器中,你可以把它理解為 LoaD Accumulator from Register,就是把某個(gè)寄存器中的值,加載到累加器中。
Star:表示 Store Accumulator Register, 你可以把它理解為 Store Accumulator to Register,就是把累加器中的值保存到某個(gè)寄存器中
Add:Add a0, [0]是從 a0 寄存器加載值并將其與累加器中的值相加,然后將結(jié)果再次放入累加器。
add a0 后面的[0]稱之為 feedback vector slot,又叫反饋向量槽,它是一個(gè)數(shù)組,解釋器將解釋執(zhí)行過(guò)程中的一些數(shù)據(jù)類型的分析信息都保存在這個(gè)反饋向量槽中了,目的是為了給 TurboFan 優(yōu)化編譯器提供優(yōu)化信息,很多字節(jié)碼都會(huì)為反饋向量槽提供運(yùn)行時(shí)信息。
LdaSmi:將小整數(shù)(Smi)加載到累加器寄存器中
Return:結(jié)束當(dāng)前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方。返回的值是累加器中的值。
隱藏類和內(nèi)聯(lián)緩存
JavaScript 是一門動(dòng)態(tài)語(yǔ)言,其執(zhí)行效率要低于靜態(tài)語(yǔ)言,V8 為了提升 JavaScript 的執(zhí)行速度,借鑒了很多靜態(tài)語(yǔ)言的特性,比如實(shí)現(xiàn)了 JIT 機(jī)制,為了提升對(duì)象的屬性訪問(wèn)速度而引入了隱藏類,為了加速運(yùn)算而引入了內(nèi)聯(lián)緩存。
為什么靜態(tài)語(yǔ)言的效率更高?  
靜態(tài)語(yǔ)言中,如 C++ 在聲明一個(gè)對(duì)象之前需要定義該對(duì)象的結(jié)構(gòu),代碼在執(zhí)行之前需要先被編譯,編譯的時(shí)候,每個(gè)對(duì)象的形狀都是固定的,也就是說(shuō),在代碼的執(zhí)行過(guò)程中是無(wú)法被改變的??梢灾苯油ㄟ^(guò)偏移量查詢來(lái)查詢對(duì)象的屬性值,這也就是靜態(tài)語(yǔ)言的執(zhí)行效率高的一個(gè)原因。  
JavaScript 在運(yùn)行時(shí),對(duì)象的屬性是可以被修改的,所以當(dāng) V8 使用了一個(gè)對(duì)象時(shí),比如使用了 obj.x 的時(shí)候,它并不知道該對(duì)象中是否有 x,也不知道 x 相對(duì)于對(duì)象的偏移量是多少,也就是說(shuō) V8 并不知道該對(duì)象的具體的形狀。那么,當(dāng)在 JavaScript 中要查詢對(duì)象 obj 中的 x 屬性時(shí),V8 會(huì)按照具體的規(guī)則一步一步來(lái)查詢,這個(gè)過(guò)程非常的慢且耗時(shí)。
將靜態(tài)的特性引入到 V8
V8 采用的一個(gè)思路就是將 JavaScript 中的對(duì)象靜態(tài)化,也就是 V8 在運(yùn)行 JavaScript 的過(guò)程中,會(huì)假設(shè) JavaScript 中的對(duì)象是靜態(tài)的。
具體地講,V8 對(duì)每個(gè)對(duì)象做如下兩點(diǎn)假設(shè):
對(duì)象創(chuàng)建好了之后就不會(huì)添加新的屬性;
對(duì)象創(chuàng)建好了之后也不會(huì)刪除屬性。
符合這兩個(gè)假設(shè)之后,V8 就可以對(duì) JavaScript 中的對(duì)象做深度優(yōu)化了。V8 會(huì)為每個(gè)對(duì)象創(chuàng)建一個(gè)隱藏類,對(duì)象的隱藏類中記錄了該對(duì)象一些基礎(chǔ)的布局信息,包括以下兩點(diǎn):
對(duì)象中所包含的所有的屬性;
每個(gè)屬性相對(duì)于對(duì)象的偏移量。
有了隱藏類之后,那么當(dāng) V8 訪問(wèn)某個(gè)對(duì)象中的某個(gè)屬性時(shí),就會(huì)先去隱藏類中查找該屬性相對(duì)于它的對(duì)象的偏移量,有了偏移量和屬性類型,V8 就可以直接去內(nèi)存中取出對(duì)應(yīng)的屬性值,而不需要經(jīng)歷一系列的查找過(guò)程,那么這就大大提升了 V8 查找對(duì)象的效率。
在 V8 中,把隱藏類又稱為 map,每個(gè)對(duì)象都有一個(gè) map 屬性,其值指向內(nèi)存中的隱藏類;
map 描述了對(duì)象的內(nèi)存布局,比如對(duì)象都包括了哪些屬性,這些數(shù)據(jù)對(duì)應(yīng)于對(duì)象的偏移量是多少。
通過(guò) d8 查看隱藏類
// test.js let point1 = { x: 100, y: 200 }; let point2 = { x: 200, y: 300 }; let point3 = { x: 100 }; %DebugPrint(point1); %DebugPrint(point2); %DebugPrint(point3);
./d8 --allow-natives-syntax ./test.js
# =============== DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE] # V8 為 point1 對(duì)象創(chuàng)建的隱藏類 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) #y: 200 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE] # V8 為 point2 對(duì)象創(chuàng)建的隱藏類 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 200 (const data field 0) #y: 300 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE] # V8 為 point3 對(duì)象創(chuàng)建的隱藏類 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) } 0x1ea308284d39: [Map] - type: JS_OBJECT_TYPE - instance size: 16 - inobject properties: 1 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
多個(gè)對(duì)象共用一個(gè)隱藏類
在 V8 中,每個(gè)對(duì)象都有一個(gè) map 屬性,該屬性值指向該對(duì)象的隱藏類。不過(guò)如果兩個(gè)對(duì)象的形狀是相同的,V8 就會(huì)為其復(fù)用同一個(gè)隱藏類,這樣有兩個(gè)好處:
減少隱藏類的創(chuàng)建次數(shù),也間接加速了代碼的執(zhí)行速度;
減少了隱藏類的存儲(chǔ)空間。
那么,什么情況下兩個(gè)對(duì)象的形狀是相同的,要滿足以下兩點(diǎn):
相同的屬性名稱;
相等的屬性個(gè)數(shù)。
重新構(gòu)建隱藏類
給一個(gè)對(duì)象添加新的屬性,刪除新的屬性,或者改變某個(gè)屬性的數(shù)據(jù)類型都會(huì)改變這個(gè)對(duì)象的形狀,那么勢(shì)必也就會(huì)觸發(fā) V8 為改變形狀后的對(duì)象重建新的隱藏類。
// test.js let point = {}; %DebugPrint(point); point.x = 100; %DebugPrint(point); point.y = 200; %DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] ...
每次給對(duì)象添加了一個(gè)新屬性之后,該對(duì)象的隱藏類的地址都會(huì)改變,這也就意味著隱藏類也隨著改變了;如果刪除對(duì)象的某個(gè)屬性,那么對(duì)象的形狀也就隨著發(fā)生了改變,這時(shí) V8 也會(huì)重建該對(duì)象的隱藏類;
最佳實(shí)踐
使用字面量初始化對(duì)象時(shí),要保證屬性的順序是一致的;
盡量使用字面量一次性初始化完整對(duì)象屬性;
盡量避免使用 delete 方法。
通過(guò)內(nèi)聯(lián)緩存來(lái)提升函數(shù)執(zhí)行效率
雖然隱藏類能夠加速查找對(duì)象的速度,但是在 V8 查找對(duì)象屬性值的過(guò)程中,依然有查找對(duì)象的隱藏類和根據(jù)隱藏類來(lái)查找對(duì)象屬性值的過(guò)程。如果一個(gè)函數(shù)中利用了對(duì)象的屬性,并且這個(gè)函數(shù)會(huì)被多次執(zhí)行:
function loadX(obj) { return obj.x; } var obj = { x: 1, y: 3 }; var obj1 = { x: 3, y: 6 }; var obj2 = { x: 3, y: 6, z: 8 }; for (var i = 0; i < 90000; i++) { loadX(obj); loadX(obj1); // 產(chǎn)生多態(tài) loadX(obj2); }
通常 V8 獲取 obj.x 的流程:
找對(duì)象 obj 的隱藏類;
再通過(guò)隱藏類查找 x 屬性偏移量;
然后根據(jù)偏移量獲取屬性值,在這段代碼中 loadX 函數(shù)會(huì)被反復(fù)執(zhí)行,那么獲取 obj.x 的流程也需要反復(fù)被執(zhí)行;
內(nèi)聯(lián)緩存及其原理:
函數(shù) loadX 在一個(gè) for 循環(huán)里面被重復(fù)執(zhí)行了很多次,因此 V8 會(huì)想盡一切辦法來(lái)壓縮這個(gè)查找過(guò)程,以提升對(duì)象的查找效率。這個(gè)加速函數(shù)執(zhí)行的策略就是內(nèi)聯(lián)緩存 (Inline Cache),簡(jiǎn)稱為 IC;
IC 的原理:在 V8 執(zhí)行函數(shù)的過(guò)程中,會(huì)觀察函數(shù)中一些調(diào)用點(diǎn) (CallSite) 上的關(guān)鍵中間數(shù)據(jù),然后將這些數(shù)據(jù)緩存起來(lái),當(dāng)下次再次執(zhí)行該函數(shù)的時(shí)候,V8 就可以直接利用這些中間數(shù)據(jù),節(jié)省了再次獲取這些數(shù)據(jù)的過(guò)程,因此 V8 利用 IC,可以有效提升一些重復(fù)代碼的執(zhí)行效率。
IC 會(huì)為每個(gè)函數(shù)維護(hù)一個(gè)反饋向量 (FeedBack Vector),反饋向量記錄了函數(shù)在執(zhí)行過(guò)程中的一些關(guān)鍵的中間數(shù)據(jù)。
反饋向量其實(shí)就是一個(gè)表結(jié)構(gòu),它由很多項(xiàng)組成的,每一項(xiàng)稱為一個(gè)插槽 (Slot),V8 會(huì)依次將執(zhí)行 loadX 函數(shù)的中間數(shù)據(jù)寫(xiě)入到反饋向量的插槽中。
當(dāng) V8 再次調(diào)用 loadX 函數(shù)時(shí),比如執(zhí)行到 loadX 函數(shù)中的 return obj.x 語(yǔ)句時(shí),它就會(huì)在對(duì)應(yīng)的插槽中查找 x 屬性的偏移量,之后 V8 就能直接去內(nèi)存中獲取 obj.x 的屬性值了。這樣就大大提升了 V8 的執(zhí)行效率。
單態(tài)、多態(tài)和超態(tài):
如果一個(gè)插槽中只包含 1 個(gè)隱藏類,那么我們稱這種狀態(tài)為單態(tài) (monomorphic);
如果一個(gè)插槽中包含了 2 ~ 4 個(gè)隱藏類,那我們稱這種狀態(tài)為多態(tài) (polymorphic);
如果一個(gè)插槽中超過(guò) 4 個(gè)隱藏類,那我們稱這種狀態(tài)為超態(tài) (magamorphic)。
單態(tài)的性能優(yōu)于多態(tài)和超態(tài),所以我們需要稍微避免多態(tài)和超態(tài)的情況。要避免多態(tài)和超態(tài),那么就盡量默認(rèn)所有的對(duì)象屬性是不變的,比如你寫(xiě)了一個(gè) loadX(obj) 的函數(shù),那么當(dāng)傳遞參數(shù)時(shí),盡量不要使用多個(gè)不同形狀的 obj 對(duì)象。
總結(jié):
V8 引入了內(nèi)聯(lián)緩存(IC),IC 會(huì)監(jiān)聽(tīng)每個(gè)函數(shù)的執(zhí)行過(guò)程,并在一些關(guān)鍵的地方埋下監(jiān)聽(tīng)點(diǎn),這些包括了加載對(duì)象屬性 (Load)、給對(duì)象屬性賦值 (Store)、還有函數(shù)調(diào)用 (Call),V8 會(huì)將監(jiān)聽(tīng)到的數(shù)據(jù)寫(xiě)入一個(gè)稱為反饋向量 (FeedBack Vector) 的結(jié)構(gòu)中,同時(shí) V8 會(huì)為每個(gè)執(zhí)行的函數(shù)維護(hù)一個(gè)反饋向量。有了反饋向量緩存的臨時(shí)數(shù)據(jù),V8 就可以縮短對(duì)象屬性的查找路徑,從而提升執(zhí)行效率。但是針對(duì)函數(shù)中的同一段代碼,如果對(duì)象的隱藏類是不同的,那么反饋向量也會(huì)記錄這些不同的隱藏類,這就出現(xiàn)了多態(tài)和超態(tài)的情況。我們?cè)趯?shí)際項(xiàng)目中,要盡量避免出現(xiàn)多態(tài)或者超態(tài)的情況。
異步編程與消息隊(duì)列
V8 是如何執(zhí)行回調(diào)函數(shù)的
回調(diào)函數(shù)有兩種類型:同步回調(diào)和異步回調(diào),同步回調(diào)函數(shù)是在執(zhí)行函數(shù)內(nèi)部被執(zhí)行的,而異步回調(diào)函數(shù)是在執(zhí)行函數(shù)外部被執(zhí)行的。  
通用 UI 線程宏觀架構(gòu):
  
UI 線程提供一個(gè)消息隊(duì)列,并將待執(zhí)行的事件添加到消息隊(duì)列中,然后 UI 線程會(huì)不斷循環(huán)地從消息隊(duì)列中取出事件、執(zhí)行事件。關(guān)于異步回調(diào),這里也有兩種不同的類型,其典型代表是 setTimeout 和 XMLHttpRequest:
setTimeout 的執(zhí)行流程其實(shí)是比較簡(jiǎn)單的,在 setTimeout 函數(shù)內(nèi)部封裝回調(diào)消息,并將回調(diào)消息添加進(jìn)消息隊(duì)列,然后主線程從消息隊(duì)列中取出回調(diào)事件,并執(zhí)行回調(diào)函數(shù)。
XMLHttpRequest 稍微復(fù)雜一點(diǎn),因?yàn)橄螺d過(guò)程需要放到單獨(dú)的一個(gè)線程中去執(zhí)行,所以執(zhí)行 XMLHttpRequest.send 的時(shí)候,宿主會(huì)將實(shí)際請(qǐng)求轉(zhuǎn)發(fā)給網(wǎng)絡(luò)線程,然后 send 函數(shù)退出,主線程繼續(xù)執(zhí)行下面的任務(wù)。網(wǎng)絡(luò)線程在執(zhí)行下載的過(guò)程中,會(huì)將一些中間信息和回調(diào)函數(shù)封裝成新的消息,并將其添加進(jìn)消息隊(duì)列中,然后主線程從消息隊(duì)列中取出回調(diào)事件,并執(zhí)行回調(diào)函數(shù)。
宏任務(wù)和微任務(wù)
調(diào)用棧:調(diào)用棧是一種數(shù)據(jù)結(jié)構(gòu),用來(lái)管理在主線程上執(zhí)行的函數(shù)的調(diào)用關(guān)系。主線程在執(zhí)行任務(wù)的過(guò)程中,如果函數(shù)的調(diào)用層次過(guò)深,可能造成棧溢出的錯(cuò)誤,我們可以使用 setTimeout 來(lái)解決棧溢出的問(wèn)題。setTimeout 的本質(zhì)是將同步函數(shù)調(diào)用改成異步函數(shù)調(diào)用,這里的異步調(diào)用是將回調(diào)函數(shù)封裝成宏任務(wù),并將其添加進(jìn)消息隊(duì)列中,然后主線程再按照一定規(guī)則循環(huán)地從消息隊(duì)列中讀取下一個(gè)宏任務(wù)。
宏任務(wù):就是指消息隊(duì)列中的等待被主線程執(zhí)行的事件。每個(gè)宏任務(wù)在執(zhí)行時(shí),V8 都會(huì)重新創(chuàng)建棧,然后隨著宏任務(wù)中函數(shù)調(diào)用,棧也隨之變化,最終,當(dāng)該宏任務(wù)執(zhí)行結(jié)束時(shí),整個(gè)棧又會(huì)被清空,接著主線程繼續(xù)執(zhí)行下一個(gè)宏任務(wù)。
微任務(wù):你可以把微任務(wù)看成是一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束之前。
JavaScript 中之所以要引入微任務(wù),主要是由于主線程執(zhí)行消息隊(duì)列中宏任務(wù)的時(shí)間顆粒度太粗了,無(wú)法勝任一些對(duì)精度和實(shí)時(shí)性要求較高的場(chǎng)景,微任務(wù)可以在實(shí)時(shí)性和效率之間做一個(gè)有效的權(quán)衡。另外使用微任務(wù),可以改變我們現(xiàn)在的異步編程模型,使得我們可以使用同步形式的代碼來(lái)編寫(xiě)異步調(diào)用。
微任務(wù)是基于消息隊(duì)列、事件循環(huán)、UI 主線程還有堆棧而來(lái)的,然后基于微任務(wù),又可以延伸出協(xié)程、Promise、Generator、await/async 等現(xiàn)代前端經(jīng)常使用的一些技術(shù)。
// 不會(huì)使瀏覽器卡死 function foo() { setTimeout(foo, 0); } foo();
微任務(wù):
// 瀏覽器console控制臺(tái)可使瀏覽器卡死(無(wú)法響應(yīng)鼠標(biāo)事件等) function foo() { return Promise.resolve().then(foo); } foo();
如果當(dāng)前的任務(wù)中產(chǎn)生了一個(gè)微任務(wù),通過(guò) Promise.resolve() 或者 Promise.reject() 都會(huì)觸發(fā)微任務(wù),觸發(fā)的微任務(wù)不會(huì)在當(dāng)前的函數(shù)中被執(zhí)行,所以*執(zhí)行微任務(wù)時(shí),不會(huì)導(dǎo)致棧的無(wú)限擴(kuò)張;
和異步調(diào)用不同,微任務(wù)依然會(huì)在當(dāng)前任務(wù)執(zhí)行結(jié)束之前被執(zhí)行,這也就意味著在當(dāng)前微任務(wù)執(zhí)行結(jié)束之前,消息隊(duì)列中的其他任務(wù)是不可能被執(zhí)行的。因此在函數(shù)內(nèi)部觸發(fā)的微任務(wù),一定比在函數(shù)內(nèi)部觸發(fā)的宏任務(wù)要優(yōu)先執(zhí)行。
微任務(wù)依然是在當(dāng)前的任務(wù)中執(zhí)行的,所以如果在微任務(wù)中循環(huán)觸發(fā)新的微任務(wù),那么將導(dǎo)致消息隊(duì)列中的其他任務(wù)沒(méi)有機(jī)會(huì)被執(zhí)行。
前端異步編程方案史
Callback 模式的異步編程模型需要實(shí)現(xiàn)大量的回調(diào)函數(shù),大量的回調(diào)函數(shù)會(huì)打亂代碼的正常邏輯,使得代碼變得不線性、不易閱讀,這就是我們所說(shuō)的回調(diào)地獄問(wèn)題。
Promise 能很好地解決回調(diào)地獄的問(wèn)題,我們可以按照線性的思路來(lái)編寫(xiě)代碼,這個(gè)過(guò)程是線性的,非常符合人的直覺(jué)。
但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較復(fù)雜的話,那么整段代碼將充斥著大量的 then,語(yǔ)義化不明顯,代碼不能很好地表示執(zhí)行流程。我們想要通過(guò)線性的方式來(lái)編寫(xiě)異步代碼,要實(shí)現(xiàn)這個(gè)理想,最關(guān)鍵的是要能實(shí)現(xiàn)函數(shù)暫停和恢復(fù)執(zhí)行的功能。而生成器就可以實(shí)現(xiàn)函數(shù)暫停和恢復(fù),我們可以在生成器中使用同步代碼的邏輯來(lái)異步代碼 (實(shí)現(xiàn)該邏輯的核心是協(xié)程)。
但是在生成器之外,我們還需要一個(gè)觸發(fā)器來(lái)驅(qū)動(dòng)生成器的執(zhí)行。前端的最終方案就是 async/await,async 是一個(gè)可以暫停和恢復(fù)執(zhí)行的函數(shù),在 async 函數(shù)內(nèi)部使用 await 來(lái)暫停 async 函數(shù)的執(zhí)行,await 等待的是一個(gè) Promise 對(duì)象,如果 Promise 的狀態(tài)變成 resolve 或者 reject,那么 async 函數(shù)會(huì)恢復(fù)執(zhí)行。因此,使用 async/await 可以實(shí)現(xiàn)以同步的方式編寫(xiě)異步代碼這一目標(biāo)。和生成器函數(shù)一樣,使用了 async 聲明的函數(shù)在執(zhí)行時(shí),也是一個(gè)單獨(dú)的協(xié)程,我們可以使用 await 來(lái)暫停該協(xié)程,由于 await 等待的是一個(gè) Promise 對(duì)象,我們可以 resolve 來(lái)恢復(fù)該協(xié)程。
協(xié)程是一種比線程更加輕量級(jí)的存在。你可以把協(xié)程看成是跑在線程上的任務(wù),一個(gè)線程上可以存在多個(gè)協(xié)程,但是在線程上同時(shí)只能執(zhí)行一個(gè)協(xié)程。比如,當(dāng)前執(zhí)行的是 A 協(xié)程,要啟動(dòng) B 協(xié)程,那么 A 協(xié)程就需要將主線程的控制權(quán)交給 B 協(xié)程,這就體現(xiàn)在 A 協(xié)程暫停執(zhí)行,B 協(xié)程恢復(fù)執(zhí)行;同樣,也可以從 B 協(xié)程中啟動(dòng) A 協(xié)程。通常,如果從 A 協(xié)程啟動(dòng) B 協(xié)程,我們就把 A 協(xié)程稱為 B 協(xié)程的父協(xié)程。
正如一個(gè)進(jìn)程可以擁有多個(gè)線程一樣,一個(gè)線程也可以擁有多個(gè)協(xié)程。每一時(shí)刻,該線程只能執(zhí)行其中某一個(gè)協(xié)程。最重要的是,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)。這樣帶來(lái)的好處就是性能得到了很大的提升,不會(huì)像線程切換那樣消耗資源。
垃圾回收
垃圾數(shù)據(jù)  
從“GC Roots”對(duì)象出發(fā),遍歷 GC Root 中的所有對(duì)象,如果通過(guò) GC Roots 沒(méi)有遍歷到的對(duì)象,則這些對(duì)象便是垃圾數(shù)據(jù)。V8 會(huì)有專門的垃圾回收器來(lái)回收這些垃圾數(shù)據(jù)。
垃圾回收算法
垃圾回收大致可以分為以下幾個(gè)步驟:
第一步,通過(guò) GC Root 標(biāo)記空間中活動(dòng)對(duì)象和非活動(dòng)對(duì)象。目前 V8 采用的可訪問(wèn)性(reachability)算法來(lái)判斷堆中的對(duì)象是否是活動(dòng)對(duì)象。具體地講,這個(gè)算法是將一些 GC Root 作為初始存活的對(duì)象的集合,從 GC Roots 對(duì)象出發(fā),遍歷 GC Root 中的所有對(duì)象:
全局的 window 對(duì)象(位于每個(gè) iframe 中);
文檔 DOM 樹(shù),由可以通過(guò)遍歷文檔到達(dá)的所有原生 DOM 節(jié)點(diǎn)組成;
存放棧上變量。
通過(guò) GC Root 遍歷到的對(duì)象,我們就認(rèn)為該對(duì)象是可訪問(wèn)的(reachable),那么必須保證這些對(duì)象應(yīng)該在內(nèi)存中保留,我們也稱可訪問(wèn)的對(duì)象為活動(dòng)對(duì)象;
通過(guò) GC Roots 沒(méi)有遍歷到的對(duì)象,則是不可訪問(wèn)的(unreachable),那么這些不可訪問(wèn)的對(duì)象就可能被回收,我們稱不可訪問(wèn)的對(duì)象為非活動(dòng)對(duì)象。
在瀏覽器環(huán)境中,GC Root 有很多,通常包括了以下幾種 (但是不止于這幾種):
第二步,回收非活動(dòng)對(duì)象所占據(jù)的內(nèi)存。其實(shí)就是在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有被標(biāo)記為可回收的對(duì)象。
第三步,做內(nèi)存整理。一般來(lái)說(shuō),頻繁回收對(duì)象后,內(nèi)存中就會(huì)存在大量不連續(xù)空間,我們把這些不連續(xù)的內(nèi)存空間稱為內(nèi)存碎片。當(dāng)內(nèi)存中出現(xiàn)了大量的內(nèi)存碎片之后,如果需要分配較大的連續(xù)內(nèi)存時(shí),就有可能出現(xiàn)內(nèi)存不足的情況,所以最后一步需要整理這些內(nèi)存碎片。但這步其實(shí)是可選的,因?yàn)橛械睦厥掌鞑粫?huì)產(chǎn)生內(nèi)存碎片(比如副垃圾回收器)。
垃圾回收
V8 依據(jù)代際假說(shuō),將堆內(nèi)存劃分為新生代和老生代兩個(gè)區(qū)域,新生代中存放的是生存時(shí)間短的對(duì)象,老生代中存放生存時(shí)間久的對(duì)象。代際假說(shuō)有兩個(gè)特點(diǎn):
第一個(gè)是大部分對(duì)象都是“朝生夕死”的,也就是說(shuō)大部分對(duì)象在內(nèi)存中存活的時(shí)間很短,比如函數(shù)內(nèi)部聲明的變量,或者塊級(jí)作用域中的變量,當(dāng)函數(shù)或者代碼塊執(zhí)行結(jié)束時(shí),作用域中定義的變量就會(huì)被銷毀。因此這一類對(duì)象一經(jīng)分配內(nèi)存,很快就變得不可訪問(wèn);
第二個(gè)是不死的對(duì)象,會(huì)活得更久,比如全局的 window、DOM、Web API 等對(duì)象。
為了提升垃圾回收的效率,V8 設(shè)置了兩個(gè)垃圾回收器,主垃圾回收器和副垃圾回收器。
主垃圾回收器主要負(fù)責(zé)老生代中的垃圾回收。除了新生代中晉升的對(duì)象,一些大的對(duì)象會(huì)直接被分配到老生代里。
老生代中的對(duì)象有兩個(gè)特點(diǎn):一個(gè)是對(duì)象占用空間大;另一個(gè)是對(duì)象存活時(shí)間長(zhǎng)。
這種角色翻轉(zhuǎn)的操作還能讓新生代中的這兩塊區(qū)域無(wú)限重復(fù)使用下去。
副垃圾回收器每次執(zhí)行清理操作時(shí),都需要將存活的對(duì)象從對(duì)象區(qū)域復(fù)制到空閑區(qū)域,復(fù)制操作需要時(shí)間成本,如果新生區(qū)空間設(shè)置得太大了,那么每次清理的時(shí)間就會(huì)過(guò)久,所以為了執(zhí)行效率,一般新生區(qū)的空間會(huì)被設(shè)置得比較小。
副垃圾回收器還會(huì)采用對(duì)象晉升策略,也就是移動(dòng)那些經(jīng)過(guò)兩次垃圾回收依然還存活的對(duì)象到老生代中。
主垃圾回收器負(fù)責(zé)收集老生代中的垃圾數(shù)據(jù),副垃圾回收器負(fù)責(zé)收集新生代中的垃圾數(shù)據(jù)。
副垃圾回收器采用了 Scavenge 算法,是把新生代空間對(duì)半劃分為兩個(gè)區(qū)域(有些地方也稱作From和To空間),一半是對(duì)象區(qū)域,一半是空閑區(qū)域。新的數(shù)據(jù)都分配在對(duì)象區(qū)域,等待對(duì)象區(qū)域快分配滿的時(shí)候,垃圾回收器便執(zhí)行垃圾回收操作,之后將存活的對(duì)象從對(duì)象區(qū)域拷貝到空閑區(qū)域,并將兩個(gè)區(qū)域互換。
主垃圾回收器回收器主要負(fù)責(zé)老生代中的垃圾數(shù)據(jù)的回收操作,會(huì)經(jīng)歷標(biāo)記、清除和整理過(guò)程。
Stop-The-World
由于 JavaScript 是運(yùn)行在主線程之上的,因此,一旦執(zhí)行垃圾回收算法,都需要將正在執(zhí)行的 JavaScript 腳本暫停下來(lái),待垃圾回收完畢后再恢復(fù)腳本執(zhí)行。我們把這種行為叫做全停頓(Stop-The-World)。
V8 最開(kāi)始的垃圾回收器有兩個(gè)特點(diǎn):
第一個(gè)是垃圾回收在主線程上執(zhí)行,
第二個(gè)特點(diǎn)是一次執(zhí)行一個(gè)完整的垃圾回收流程。
由于這兩個(gè)原因,很容易造成主線程卡頓,所以 V8 采用了很多優(yōu)化執(zhí)行效率的方案。
第一個(gè)方案是并行回收,在執(zhí)行一個(gè)完整的垃圾回收過(guò)程中,垃圾回收器會(huì)使用多個(gè)輔助線程來(lái)并行執(zhí)行垃圾回收。
第二個(gè)方案是增量式垃圾回收,垃圾回收器將標(biāo)記工作分解為更小的塊,并且穿插在主線程不同的任務(wù)之間執(zhí)行。采用增量垃圾回收時(shí),垃圾回收器沒(méi)有必要一次執(zhí)行完整的垃圾回收過(guò)程,每次執(zhí)行的只是整個(gè)垃圾回收過(guò)程中的一小部分工作。
第三個(gè)方案是并發(fā)回收,回收線程在執(zhí)行 JavaScript 的過(guò)程,輔助線程能夠在后臺(tái)完成的執(zhí)行垃圾回收的操作。
主垃圾回收器就綜合采用了所有的方案(并發(fā)標(biāo)記,增量標(biāo)記,輔助清理),副垃圾回收器也采用了部分方案。
似此星辰非昨夜,為誰(shuí)風(fēng)露立中宵
Breaking the JavaScript Speed Limit with V8  
Daniel Clifford 在 Google I/O 2012 上做了一個(gè)精彩的演講“Breaking the JavaScript Speed Limit with V8”。在演講中,他深入解釋了 13 個(gè)簡(jiǎn)單的代碼優(yōu)化方法,可以讓你的JavaScript代碼在 Chrome V8 引擎編譯/運(yùn)行時(shí)更加快速。在演講中,他介紹了怎么優(yōu)化,并解釋了原因。下面簡(jiǎn)明的列出了13 個(gè) JavaScript 性能提升技巧:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
在構(gòu)造函數(shù)里初始化所有對(duì)象的成員(所以這些實(shí)例之后不會(huì)改變其隱藏類);
總是以相同的次序初始化對(duì)象成員;
盡量使用可以用 31 位有符號(hào)整數(shù)表示的數(shù);
為數(shù)組使用從 0 開(kāi)始的連續(xù)的主鍵;
別預(yù)分配大數(shù)組(比如大于 64K 個(gè)元素)到其最大尺寸,令其尺寸順其自然發(fā)展就好;
別刪除數(shù)組里的元素,尤其是數(shù)字?jǐn)?shù)組;
別加載未初始化或已刪除的元素;
對(duì)于固定大小的數(shù)組,使用”array literals“初始化(初始化小額定長(zhǎng)數(shù)組時(shí),用字面量進(jìn)行初始化);
小數(shù)組(小于 64k)在使用之前先預(yù)分配正確的尺寸;
請(qǐng)勿在數(shù)字?jǐn)?shù)組中存放非數(shù)字的值(對(duì)象);
盡量使用單一類型(monomorphic)而不是多類型(polymorphic)(如果通過(guò)非字面量進(jìn)行初始化小數(shù)組時(shí),切勿觸發(fā)類型的重新轉(zhuǎn)換);
不要使用 try{} catch{}(如果存在 try/catch 代碼快,則將性能敏感的代碼放到一個(gè)嵌套的函數(shù)中);
在優(yōu)化后避免在方法中修改隱藏類。
在 V8 引擎里 5 個(gè)優(yōu)化代碼的技巧
1. 對(duì)象屬性的順序: 在實(shí)例化你的對(duì)象屬性的時(shí)候一定要使用相同的順序,這樣隱藏類和隨后的優(yōu)化代碼才能共享;
2. 動(dòng)態(tài)屬性: 在對(duì)象實(shí)例化之后再添加屬性會(huì)強(qiáng)制使得隱藏類變化,并且會(huì)減慢為舊隱藏類所優(yōu)化的代碼的執(zhí)行。所以,要在對(duì)象的構(gòu)造函數(shù)中完成所有屬性的分配;
3. 方法: 重復(fù)執(zhí)行相同的方法會(huì)運(yùn)行的比不同的方法只執(zhí)行一次要快 (因?yàn)閮?nèi)聯(lián)緩存);
4. 數(shù)組: 避免使用 keys 不是遞增的數(shù)字的稀疏數(shù)組,這種 key 值不是遞增數(shù)字的稀疏數(shù)組其實(shí)是一個(gè) hash 表。在這種數(shù)組中每一個(gè)元素的獲取都是昂貴的代價(jià)。同時(shí),要避免提前申請(qǐng)大數(shù)組。最好的做法是隨著你的需要慢慢的增大數(shù)組。最后,不要?jiǎng)h除數(shù)組中的元素,因?yàn)檫@會(huì)使得 keys 變得稀疏;
5. 標(biāo)記值 (Tagged values): V8 用 32 位來(lái)表示對(duì)象和數(shù)字。它使用一位來(lái)區(qū)分它是對(duì)象 (flag = 1) 還是一個(gè)整型 (flag = 0),也被叫做小整型(SMI),因?yàn)樗挥?31 位。然后,如果一個(gè)數(shù)值大于 31 位,V8 將會(huì)對(duì)其進(jìn)行 box 操作,然后將其轉(zhuǎn)換成 double 型,并且創(chuàng)建一個(gè)新的對(duì)象來(lái)裝這個(gè)數(shù)。所以,為了避免代價(jià)很高的 box 操作,盡量使用 31 位的有符號(hào)數(shù)。
JavaScript 啟動(dòng)性能瓶頸分析與解決方案
資料參考:
JavaScript Start-up Performance :
https://medium.com/reloading/javascript-start-up-performance-69200f43b201
JavaScript 啟動(dòng)性能瓶頸分:析與解決方案:
https://zhuanlan.zhihu.com/p/25221314
抽絲剝繭有窮時(shí),V8 綿綿無(wú)絕期
v8官方文檔(https://v8.dev/)
圖解 Google V8(https://time.geekbang.org/column/intro/296)
瀏覽器工作原理與實(shí)踐(https://time.geekbang.org/column/intro/216)
[[譯] JavaScript 如何工作:對(duì)引擎、運(yùn)行時(shí)、調(diào)用堆棧的概述]:https://juejin.im/post/6844903510538993671)
[[譯] JavaScript 如何工作的: 事件循環(huán)和異步編程的崛起 + 5 個(gè)關(guān)于如何使用 async/await 編寫(xiě)更好的技巧](https://juejin.im/post/6844903518319411207)
番外篇
Console Importer:Easily import JS and CSS resources from Chrome console. (可以在瀏覽器控制臺(tái)安裝 loadsh、moment、jQuery 等庫(kù),在控制臺(tái)直接驗(yàn)證、使用這些庫(kù)。)
效果圖:
看完上述內(nèi)容,你們掌握由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。