V8 是一個由 Google 開發的開源 JavaScript 引擎,目前用在 Chrome 瀏覽器和 Node.js 中,其核心功能是執行易於人類理解的 JavaScript 代碼。javascript
V8 採用混合使用編譯器和解釋器的技術,稱爲 JIT(Just In Time)技術。前端
下面是 V8 執行 JavaScript 代碼的流程圖:java
先了解下相關概念
這裏的棧空間就是調用棧(Call Stack),是用來存儲執行上下文的。node
在函數調用過程當中,涉及到上下文相關的內容都會存放在棧上,好比原始類型、引用到的對象的地址、函數的執行狀態、this 值等都會存在在棧上。當一個
函數執行結束,那麼該函數的執行上下文便會被銷燬掉。segmentfault
咱們知道,大部分高級語言都不約而同地採用棧這種結構來管理函數調用,爲何呢?這與函數的特性有關。一般函數有兩個主要的特性:數組
咱們能夠先看下面這段 C 代碼:瀏覽器
int getZ() { return 4; } int add(int x, int y) { int z = getZ(); return x + y + z; } int main() { int x = 5; int y = 6; int ret = add(x, y); }
具體的函數調用示意圖以下:數據結構
咱們能夠得出,函數調用者的生命週期老是長於被調用者(後進),而且被調用者的生命週期老是先於調用者的生命週期結束 (先出)。閉包
由於函數是有做用域機制的,做用域機制一般表如今函數執行時,會在內存中分配函數內部的變量、上下文等數據,在函數執行完成以後,這些內部數據會被銷燬掉。架構
因此站在函數資源分配和回收角度來看,被調用函數的資源分配老是晚於調用函數 (後進),而函數資源的釋放則老是先於調用函數 (先出)。以下圖所示:
經過觀察函數的生命週期和函數的資源分配狀況,咱們發現,它們都符合後進先出 (LIFO) 的策略,而棧結構正好知足這種後進先出 (LIFO) 的需求,因此咱們選擇棧來管理函數調用關係是一種很天然的選擇。
當一個函數被執行時,函數的參數、函數內部定義變量都會依次壓入到棧中,咱們結合實際的代碼來分析下這個過程,你能夠參考下圖:
你會發現,函數在執行過程當中,其內部的臨時變量會按照執行順序被壓入到棧中。
來看一下複雜一點的場景:
int add(num1, num2) { int x = num1; int y = num2; int ret = x + y; return ret; } int main() { int x = 5; int y = 6; x = 100; int z = add(x, y); return z; }
咱們把上段代碼中的 x+y 改形成了一個 add 函數,當執行到 int z = add(x,y) 時,當前棧的狀態以下所示:
接下來,就要調用 add 函數了,理想狀態下,執行 add 函數的過程是下面這樣的:
當執行到 add 函數時,會先把參數 num1 和 num2 壓棧,接着咱們再把變量 x、y、ret 的值依次壓棧,不過執行這裏,會遇到一個問題,那就是當 add 函數執行完成以後,須要將執行代碼的控制權轉交給 main 函數,這意味着須要將棧的狀態恢復到 main 函數上次執行時的狀態,咱們把這個過程叫恢復現場。
那麼應該怎麼恢復 main 函數的執行現場呢?
其實方法很簡單,只要在寄存器中保存一個永遠指向當前棧頂的指針,棧頂指針的做用就是告訴你應該往哪一個位置添加新元素,這個指針一般存放在 esp 寄存器中。若是你想往棧中添加一個元素,那麼你須要先根據 esp 寄存器找到當前棧頂的位置,而後在棧頂上方添加新元素,新元素添加以後,還須要將新元素的地址更新到 esp 寄存器中。
有了棧頂指針,就很容易恢復 main 函數的執行現場了,當 add 函數執行結束時,只須要將棧頂指針向下移動就能夠了,具體你能夠參看下圖:
add函數即將執行結束的狀態
恢復mian函數執行現場
觀察上圖,將 esp 的指針向下移動到以前 main 函數執行時的地方就能夠,不過新的問題又來了,CPU 是怎麼知道要移動到這個地址呢?
CPU 的解決方法是增長了另一個 ebp 寄存器,用來保存當前函數的起始位置,咱們把一個函數的起始位置也稱爲棧幀指針,ebp 寄存器中保存的就是當前函數的棧幀指針,以下圖所示:
在 main 函數調用 add 函數的時候,main 函數的棧頂指針就變成了 add 函數的棧幀指針,因此須要將 main 函數的棧頂指針保存到 ebp 中,當 add 函數執行結束以後,我須要銷燬 add 函數的棧幀,並恢復 main 函數的棧幀,那麼只須要取出 main 函數的棧頂指針寫到 esp 中便可 (main 函數的棧頂指針是保存在 ebp 中的),這就至關於將棧頂指針移動到 main 函數的區域。
那麼如今,咱們能夠執行 main 函數了嗎?
答案依然是「不能」,這主要是由於 main 函數也有它本身的棧幀指針,在執行 main 函數以前,咱們還需恢復它的棧幀指針。如何恢復 main 函數的棧幀指針呢?
一般的方法是在 main 函數中調用 add 函數時,CPU 會將當前 main 函數的棧幀指針保存在棧中,以下圖所示:
當函數調用結束以後,就須要恢復 main 函數的執行現場了,首先取出 ebp 中的指針,寫入 esp 中,而後從棧中取出以前保留的 main 的棧幀地址,將其寫入 ebp 中,到了這裏 ebp 和 esp 就都恢復了,能夠繼續執行 main 函數了。
另外在這裏,咱們還須要補充下棧幀的概念,由於在不少文章中咱們會看到這個概念,每一個棧幀對應着一個未運行完的函數,棧幀中保存了該函數的返回地址和局部變量。
以上咱們詳細分析了 C 函數的執行過程,在 JavaScript 中,函數的執行過程也是相似的,若是調用一個新函數,那麼 V8 會爲該函數建立棧幀,等函數執行結束以後,銷燬該棧幀,而棧結構的容量是固定的,全部若是重複嵌套執行一個函數,那麼就會致使棧會棧溢出。
好了,咱們如今理解了棧是怎麼管理函數調用的了,使用棧有很是多的優點:
雖然操做速度很是快,可是棧也是有缺點的,其中最大的缺點也是它的優勢所形成的,那就是棧是連續的,因此要想在內存中分配一塊連續的大空間是很是難的,所以棧空間是有限的。
由於棧空間是有限的,這就致使咱們在編寫程序的時候,常常一不當心就會致使棧溢出,好比函數循環嵌套層次太多,或者在棧上分配的數據過大,都會致使棧溢出,基於棧不方便存放大的數據,所以咱們使用了另一種數據結構用來保存一些大數據,這就是堆。
堆空間是一種樹形的存儲結構,用來存儲對象類型的離散的數據,JavaScript 中除了原始類型的數據,其餘的都是對象類型,諸如函數、數組,在瀏覽器中還有 window 對象、document 對象等,這些都是存在堆空間的。
和棧空間不一樣,存放在堆空間中的數據是不要求連續存放的,從堆上分配內存塊沒有固定模式的,你能夠在任什麼時候候分配和釋放它,爲了更好地理解堆,咱們看下面這段代碼是怎麼執行的:
struct Point { int x; int y; }; int main() { int x = 5; int y = 6; int *z = new int; *z = 20; Point p; p.x = 100; p.y = 200; Point *pp = new Point(); pp->y = 400; pp->x = 500; delete z; delete pp; return 0; }
觀察上面這段代碼,你能夠看到代碼中有 new int、new Point 這種語句,當執行這些語句時,表示要在堆中分配一塊數據,而後返回指針,一般返回的指針會被保存到棧中,下面咱們來看看當 main 函數快執行結束時,堆和棧的狀態,具體內容你能夠參看下圖:
觀察上圖,咱們能夠發現,當使用 new 時,咱們會在堆中分配一塊空間,在堆中分配空間以後,會返回分配後的地址,咱們會把該地址保存在棧中,如上圖中 z 和 pp 都是地址,它們保存在棧中,指向了在堆中分配的空間。
一般,當堆中的數據再也不須要的時候,須要對其進行銷燬,在 C 語言中可使用 free,在 C++ 語言中可使用 delete 來進行操做。
JavaScript,Java 使用了自動垃圾回收策略,能夠實現垃圾自動回收,可是事情總有兩面性,垃圾自動回收也會給咱們帶來一些性能問題。
簡而言之,執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行。
執行上下文總共有三種類型:
在任意的 JavaScript 代碼被執行前,執行上下文處於建立階段。在建立階段中總共發生了兩件事情:
所以,執行上下文能夠在概念上表示以下:
ExecutionContext = { LexicalEnvironment = { ... }, VariableEnvironment = { ... }, }
官方 ES6 文檔將詞法環境定義爲:
詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能爲空引用(null)的外部詞法環境以及 this binding 組成。
簡而言之,詞法環境是一個包含標識符變量映射的結構。(這裏的標識符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用)。
例如:
var a = 20; var b = 40; function foo() { console.log('bar'); }
上面的詞法環境看起來像這樣:
lexicalEnvironment = { a: 20, b: 40, foo: <ref. to foo function> }
在詞法環境中,有三個組成部分:
環境記錄
是存儲變量和函數聲明的實際位置。
環境記錄 一樣有兩種類型(以下所示):
function code
的詞法環境包含一個聲明性環境記錄。global code
的詞法環境包含一個對象環境記錄。除了變量和函數聲明外,對象環境記錄還存儲一個global binding object
(在瀏覽器中是 window 對象)。所以,對於每個綁定對象屬性(在瀏覽器中,它包含瀏覽器窗口對象提供的屬性和方法),在記錄中建立一個新條目。
對於函數代碼,環境記錄該對象包含了索引和傳遞給函數的參數之間的映射以及傳遞給函數的參數的
長度(數量)。例如,下面函數的
arguments
對象以下所示:
function foo(a, b) { var c = a + b; } foo(2, 3); // argument object Arguments: {0: 2, 1: 3, length: 2},
對外部環境的引用
對外部環境的引用意味着它能夠訪問其父級詞法環境(做用域)。這意味着若是在當前詞法環境找不到變量,JavaScript引擎就會在父級詞法做用域尋找。
This Binding
在全局執行上下文中,this
的值指向全局對象(在瀏覽器中,this
的值指向 window 對象)。
在函數執行上下文中,this
的值取決於函數的調用方式。若是它被一個對象引用調用,那麼 this
的值被設置爲該對象,不然 this
的值被設置爲全局對象或 undefined
(嚴格模式下)。例如:
const person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 'this' refers to 'person', because 'calcAge' was called with //'person' object reference const calculateAge = person.calcAge; calculateAge(); // 'this' refers to the global window object, because no object reference was given
抽象來看,詞法環境看起來像這樣的僞代碼:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here } outer: <null>, this: <global object> } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here } outer: <Global or outer function environment reference>, this: <depends on how function is called> } }
詳細能夠看以前[[JavaScript總結]this綁定全面解析](https://segmentfault.com/a/11...
它也是一個詞法環境,其 EnvironmentRecord
包含了由 VariableStatements 在此執行上下文建立的綁定。
如上所述,變量環境也是一個詞法環境,所以它具備上面定義的詞法環境的全部屬性。
在 ES6 中,LexicalEnvironment 組件和 VariableEnvironment 組件的區別在於前者用於存儲函數聲明和變量( let
和 const
)綁定,然後者僅用於存儲變量( var
)綁定。
在 ES2018 中,執行上下文又變成了這個樣子,this 值被納入 lexical environment,可是增長了很多內容。
- lexical environment:詞法環境,當獲取變量或者 this 值時使用。
- variable environment:變量環境,當聲明變量時使用。
- code evaluation state:用於恢復代碼執行位置。
- Function:執行的任務是函數時使用,表示正在被執行的函數。
- ScriptOrModule:執行的任務是腳本或者模塊時使用,表示正在被執行的代碼。
- Realm:使用的基礎庫和內置對象實例。
- Generator:僅生成器上下文有這個屬性,表示當前生成器。
做用域是指在程序中定義變量的區域,該位置決定了變量的生命週期。通俗地理解,做用域就是變量與函數的可訪問範圍,即做用域控制着變量和函數的可見性和生命週期。
ECMAScript 的做用域有三種:
let
和const
聲明,聲明後的變量再指定塊級做用域塊外沒法被訪問。所謂的變量提高,是指在JavaScript代碼執行過程當中,JavaScript引擎把變量的聲明部分和函數的聲明部分提高到代碼開頭的「行爲」。變量被提高後,會給變量設置默認值,這個默認值就是咱們熟悉的undefined。
看一下下面這段代碼:
showName() console.log(myname) var myname = 'JavaScript' function showName() { console.log('函數showName被執行'); }
分析下上面的代碼:
通過編譯後,會生成兩部份內容:執行上下文(Execution context)和可執行代碼。
//執行上下文的變量環境保存了變量提高的內容,也就是myname變量,詞法環境保存了showName()。 var myname = undefined function showName() { console.log('函數showName被執行'); } //可執行代碼 showName() console.log(myname) // undefined myname = 'JavaScript'
JavaScript引擎開始執行「可執行代碼」,按照順序一行一行地執行。
var myname = "JavaScript" function showName(){ console.log(myname); if(0){ var myname = "CSS" } console.log(myname); } showName() //undefined
function foo(){ for (var i = 0; i < 7; i++) { } console.log(i); } foo() //7,由於變量提高,for循環結束的時候 i 沒有被銷燬
因此爲了解決這個問題,引用了 塊級做用域。
其實在每一個執行上下文的詞法(變量)環境中,都包含了一個外部引用,用來指向外部的執行上下文,咱們把這個外部引用稱爲 outer 。
看下面這段代碼:
function bar() { console.log(myName) } function foo() { var myName = " CSS " bar() } var myName = " JavaScript " foo() // JavaScript
從圖中能夠看出,bar 函數和 foo 函數的 outer 都是指向全局上下文的,這也就意味着若是在 bar 函數或者 foo 函數中使用了外部變量,那麼 JavaScript 引擎會去全局執行上下文中查找。咱們把這個查找的鏈條就稱爲做用域鏈。
foo 函數調用的 bar 函數,那爲何 bar 函數的外部引用是全局執行上下文,而不是 foo 函數的執行上下文?
這是由於根據詞法做用域,foo 和 bar 的上級做用域都是全局做用域,因此若是 foo 或者 bar 函數使用了一個它們沒有定義的變量,那麼它們會到全局做用域去查找。也就是說,詞法做用域是代碼階段就決定好的,和函數是怎麼調用的沒有關係。
什麼是詞法做用域呢?
詞法做用域就是指做用域是由代碼中函數聲明的位置來決定的,因此詞法做用域是靜態的做用域,經過它就可以預測代碼在執行過程當中如何查找標識符。
從圖中能夠看出,詞法做用域就是根據代碼的位置來決定的,其中 main 函數包含了 bar 函數,bar 函數中包含了 foo 函數,由於 JavaScript 做用域鏈是由詞法做用域決定的,因此整個詞法做用域鏈的順序是:foo 函數做用域—>bar 函數做用域—>main 函數做用域—> 全局做用域。
JavaScript 中的三個特性:
第一,JavaScript 語言容許在函數內部定義新的函數,代碼以下所示:
function foo() { function inner() { } inner() }
JavaScript 中之因此能夠在函數中聲明另一個函數,主要是由於 JavaScript 中的函數即對象,你能夠在函數中聲明一個變量,固然你也能夠在函數中聲明一個函數。
第二,能夠在內部函數中訪問父函數中定義的變量,代碼以下所示:
var d = 20 //inner函數的父函數,詞法做用域 function foo() { var d = 55 //foo的內部函數 function inner() { return d+2 } inner() }
因爲能夠在函數中定義新的函數,因此很天然的,內部的函數可使用外部函數中定義的變量。
第三,由於函數是一等公民(First Class Function),因此函數能夠做爲返回值,咱們能夠看下面這段代碼:
function foo() { return function inner(a, b) { const c = a + b return c } } const f = foo()
觀察上面這段代碼,咱們將 inner 函數做爲了 foo 函數的返回值,也就是說,當調用 foo 函數時,最終會返回 inner 函數給調用者,好比上面咱們將 inner 函數返回給了全局變量 f,接下來就能夠在外部像調用 inner 函數同樣調用 f 了。
瞭解了 JavaScript 的這三個特性以後,看看下面這段閉包代碼:
function foo() { var myName = " JavaScript " let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName(" CSS ") bar.getName() console.log(bar.getName()) //1 1 CSS
首先咱們看看當執行到 foo 函數內部的return innerBar這行代碼時調用棧的狀況,你能夠參考下圖:
從上面的代碼能夠看出,innerBar 是一個對象,包含了 getName 和 setName 的兩個方法(一般咱們把對象內部的函數稱爲方法)。
你能夠看到,這兩個方法都是在 foo 函數內部定義的,而且這兩個方法內部都使用了 myName 和 test1 兩個變量。
根據詞法做用域的規則,內部函數 getName 和 setName 老是能夠訪問它們的外部函數 foo 中的變量,因此當 innerBar 對象返回給全局變量 bar 時,雖然 foo 函數已經執行結束,可是 getName 和 setName 函數依然可使用 foo 函數中的變量 myName 和 test1。
因此當 foo 函數執行完成以後,其整個調用棧的狀態以下圖所示:
從上圖能夠看出,foo 函數執行完成以後,其執行上下文從棧頂彈出了,可是因爲返回的 setName 和 getName 方法中使用了 foo 函數內部的變量 myName 和 test1,因此這兩個變量依然保存在內存中。這像極了 setName 和 getName 方法背的一個專屬揹包,不管在哪裏調用了 setName 和 getName 方法,它們都會揹着這個 foo 函數的專屬揹包。
由上可知,在 JavaScript 中,根據詞法做用域的規則,內部函數老是能夠訪問其外部函數中聲明的變量,當經過調用一個外部函數返回一個內部函數後,即便該外部函數已經執行結束了,可是內部函數引用外部函數的變量依然保存在內存中,咱們就把這些變量的集合稱爲閉包。
好比外部函數是 foo,那麼這些變量的集合就稱爲 foo 函數的閉包。
V8 執行 JavaScript 代碼,須要通過編譯和執行兩個階段:
在 Chrome 中,只要打開一個渲染進程,渲染進程便會初始化 V8,同時初始化堆空間和棧空間。
若是在瀏覽器中,JavaScript 代碼會頻繁操做 window(this 默認指向 window 對象)、操做 dom 等內容,若是在 node 中,JavaScript 會頻繁使用 global(this 默認指向 global對象)、File API 等內容,這些內容都會在啓動過程當中準備好,咱們把這些內容稱之爲全局執行上下文。
在瀏覽器的環境中,全局執行上下文中就包括了 window 對象,還有默認指向 window 的 this 關鍵字,另外還有一些 Web API 函數,諸如 setTimeout、XMLHttpRequest 等內容。
全局執行上下文在 V8 的生存週期內是不會被銷燬的,它會一直保存在堆中,這樣當下次在須要使用函數或者全局變量時,就不須要從新建立了。
另外,當你執行了一段全局代碼時,若是全局代碼中有聲明的函數或者定義的變量,那麼函數對象和聲明的變量都會被添加到全局執行上下文中。
V8 啓動時,會建立全局做用域,全局做用域中包括了 this、window 等變量,還有一些全局的 Web API 接口。
你能夠把做用域當作是一個抽象的概念,好比在 ES6 中,同一個全局執行上下文中,都能存在多個做用域:
var x = 5 { let y = 2 const z = 3 }
這段代碼在執行時,就會有兩個對應的做用域,一個是全局做用域,另一個是括號內部的做用域,可是這些內容都會保存到全局執行上下文中。
有了堆空間和棧空間,生成了全局執行上下文和全局做用域,接下來就能夠執行JavaScript 代碼了嗎?
不,還須要構造事件循環系統,事件循環系統主要用來處理任務的排隊和任務的調度。
詳細內容單開文章
var name = 'Javascript' var type = 'global' function foo(){ var name = 'foo' console.log(name) console.log(type) } function bar(){ var name = 'bar' var type = 'function' foo() } bar()
高級語言是開發者能夠理解的語言,可是讓編譯器或者解釋器來理解就很是困難了。對於編譯器或者解釋器來講,它們能夠理解的就是 AST 了。因此不管你使用的是解釋型語言仍是編譯型語言,在編譯過程當中,它們都會生成一個 AST。這和渲染引擎將 HTML 格式文件轉換爲計算機能夠理解的 DOM 樹的狀況相似。
從圖中能夠看出,AST 的結構和代碼的結構很是類似,其實你也能夠把 AST 當作代碼的結構化的表示,編譯器或者解釋器後續的工做都須要依賴於 AST,而不是源代碼。
AST 是很是重要的一種數據結構,在不少項目中有着普遍的應用。其中最著名的一個項目是 Babel。Babel 是一個被普遍使用的代碼轉碼器,能夠將 ES6 代碼轉爲 ES5 代碼,這意味着你能夠如今就用 ES6 編寫程序,而不用擔憂現有環境是否支持 ES6。Babel 的工做原理就是先將 ES6 源碼轉換爲 AST,而後再將 ES6 語法的 AST 轉換爲 ES5 語法的 AST,最後利用 ES5 的 AST 生成 JavaScript 源代碼。
除了 Babel 外,還有 ESLint 也使用 AST。ESLint 是一個用來檢查 JavaScript 編寫規範的插件,其檢測流程也是須要將源碼轉換爲 AST,而後再利用 AST 來檢查代碼規範化的問題。
如今你知道了什麼是 AST 以及它的一些應用,那接下來咱們再來看下 AST 是如何生成的。一般,生成 AST 須要通過兩個階段。
第一階段是分詞(tokenize),又稱爲詞法分析,其做用是將一行行的源碼拆解成一個個 token。所謂 token,指的是語法上不可能再分的、最小的單個字符或字符串。你能夠參考下圖來更好地理解什麼 token。
從圖中能夠看出,經過 var myName = ' JavaScript '
簡單地定義了一個變量,其中關鍵字「var」、標識符「myName」 、賦值運算符「=」、字符串「 JavaScript 」四個都是 token,並且它們表明的屬性還不同。
第二階段是解析(parse),又稱爲語法分析,其做用是將上一步生成的 token 數據,根據語法規則轉爲 AST。若是源碼符合語法規則,這一步就會順利完成。但若是源碼存在語法錯誤,這一步就會終止,並拋出一個「語法錯誤」。
這就是 AST 的生成過程,先分詞,再解析。
有了 AST 後,那接下來 V8 就會生成該段代碼的執行上下文。
有了 AST 和執行上下文後,那接下來的第二步,解釋器 Ignition 就登場了,它會根據 AST 生成字節碼,並解釋執行字節碼。
其實一開始 V8 並無字節碼,而是直接將 AST 轉換爲機器碼,因爲執行機器碼的效率是很是高效的,因此這種方式在發佈後的一段時間內運行效果是很是好的。
可是隨着 Chrome 在手機上的普遍普及,特別是運行在 512M 內存的手機上,內存佔用問題也暴露出來了,由於 V8 須要消耗大量的內存來存放轉換後的機器碼。爲了解決內存佔用問題,V8 團隊大幅重構了引擎架構,引入字節碼,而且拋棄了以前的編譯器,最終花了將進四年的時間,實現瞭如今的這套架構。
那什麼是字節碼呢?爲何引入字節碼就能解決內存佔用問題呢?
字節碼就是介於 AST 和機器碼之間的一種代碼。可是與特定類型的機器碼無關,字節碼須要經過解釋器將其轉換爲機器碼後才能執行。
理解了什麼是字節碼,咱們再來對比下高級代碼、字節碼和機器碼,你能夠參考下圖
生成字節碼以後,接下來就要進入執行階段了。
此時的做用域和執行上下文:
一般,若是有一段第一次執行的字節碼,解釋器 Ignition 會逐條解釋執行。在執行字節碼的過程當中,若是發現有熱點代碼(HotSpot),好比一段代碼被重複執行屢次,這種就稱爲熱點代碼,那麼後臺的編譯器 TurboFan 就會把該段熱點的字節碼編譯爲高效的機器碼,而後當再次執行這段被優化的代碼時,只須要執行編譯後的機器碼就能夠了,這樣就大大提高了代碼的執行效率。
Understanding Execution Context and Execution Stack in Javascript