本文的主題是 JavaScript,但不是講它的功能,語法之類——相反,我要談的是JS 的工做機制,以及與此相關的一些基本術語。下面進入主題。javascript
若是你曾看過 JS 的維基百科
之類的資料,那麼確定會對一系列的術語印象深入,諸如高級(high-level)、解釋(interpreted)、JIT 編譯、動態類型、基於原型(prototype-based)等等。其中有些術語很好理解,有經驗的程序員確定早就熟悉了;但也有些看起來很陌生。並且就算你不須要了解全部這些術語也能寫代碼,這些知識也確定能夠幫助你更好地理解語言和編程。因此想要理解 JS 的工做機制,通常來講先要學習這些術語的含義…html
JS 開發者並不怎麼關心他們的代碼是如何工做的,或者至少沒這個必要。由於 JS 是一種高級語言。這意味着全部細節,例如數據如何存儲在存儲器(RAM)中或 CPU 執行指令的方式等,對程序員都是隱藏起來的。而「高」這個字表示的是語言提供的抽象或簡化級別。java
最底層的是機器碼。不少人都知道,機器碼只是以特定方式排列的一組 0 和 1,不一樣的排列方式對機器來講有不一樣的含義。有些可能表示特定指令、有些表示數據,諸如此類。node
機器碼上面一級是彙編語言——也是最低級編程語言,只比機器碼高級。與機器碼相比,彙編代碼的形式能夠被人類理解。也就是說你能接觸到的最底層語言就是彙編(用不着看機器碼手冊也能理解)。儘管如此,就算彙編語言具備「可讀性」,使用 ADD 或MOV等指令實際編寫彙編代碼也是一項很是艱鉅的任務。甚至你須要爲各個不一樣的目標處理器架構編寫不一樣的彙編代碼(例如桌面上的x86-64架構和移動設備上的ARM架構)!連操做系統都須要分別考慮!顯然這和咱們熟知的 JS 徹底不是一回事吧。無論怎樣,因爲彙編代碼仍然只是一個抽象,爲了運行程序也要先編譯才行,或者用一個名爲彙編器的實用程序組裝成機器碼的形式。有意思的是許多彙編器甚至不是用純彙編語言寫的,頗有趣不是嗎。程序員
從彙編語言往上走,咱們終於看到了許多人都很是熟悉的語言——最著名的是C 和 C++。在這個級別中,咱們編寫的代碼與咱們在 JS 中看到的代碼更像一些。但咱們仍然能夠訪問各類各樣的「低級」(與 JS 相比)工具,也仍然須要用這些工具本身管理(分配 / 釋放)內存。以後要經過名爲編譯器的程序將代碼(間接)編譯爲機器碼(中間會涉及彙編步驟)。注意彙編器和編譯器的區別——編譯器位於更高級別的抽象和機器代碼之間,它能作的事情比彙編器多得多。這就是爲何 C 代碼是「可移植的」,能夠編寫一次並編譯到不少平臺和架構中,相似的優點還有不少。編程
C++ 已經被認爲是一種高級語言了,那麼什麼語言更高級呢?沒錯,就是JavaScript。JS 是一種在其引擎中運行的語言,最流行的引擎是V8,這個引擎是用 C++ 編寫的。這也是爲何 JS 通常被看做是一種解釋性語言(不是徹底正確,後文會具體說明)。這意味着你編寫的 JS 代碼不會被編譯以後運行(像 C++ 那樣),而是由一個名爲解釋器的程序運行。api
如你所見,JS 確實是一種很是高級的語言。這有不少好處,主要優點在於程序員沒必要考慮那些當咱們「失敗」時就會變得可見的細節。這種高抽象級別的惟一缺點是性能損失。雖然 JS 速度很快,還在變得愈來愈快,可是你們都知道一段程序用 C++ 寫(假設它寫得很好)每每比用 JS 寫的更快。但更高層次的抽象仍是提升了開發人員的生產力,也讓編程更加輕鬆一些。這是一種折衷方案,從這裏也能看出爲何各類編程語言都有本身最適合的應用場景。瀏覽器
固然上面講的這些都只是底層機制的簡化描述,因此大概看一下就好。接下來咱們將繼續探索最高級別的抽象,也就是 JS 的工做機制。數據結構
在這份規範中有許多術語涉及到 JS 的設計及工做原理。咱們由規範得知,JS 是動態和弱類型的語言。這意味着 JS 變量的類型是隱式解析的,能夠在運行時更改(動態類型部分),而且它們不是很是嚴格地區分(弱類型部分)。所以存在像 TypeScript 這樣更高級別的抽象,而且咱們有了兩個相等運算符——一般(==)和嚴格運算符(===)。動態類型在解釋型語言中很是流行,而與之相反的靜態類型則在編譯語言中很受歡迎。多線程
關於 JS 的另外一個術語是多範式,JS 是一種多範式語言。這是由於 JS 容許你按照本身的方式編寫代碼。這意味着你的代碼能夠從聲明和函數式變爲命令式和麪向對象類型,甚至能夠混合使用這兩種範式。編程範式的話題很大,深刻探討就要另開新文了。
那麼 JS 是如何實現「多範式」的呢?這裏就要引入另外一個對 JS 相當重要的概念——原型繼承。如今你可能已經知道 JS 中的全部事物都是一個對象。你可能還知道面向對象編程和基於類的繼承這些術語都是什麼意思。接下來你必須知道,雖然原型繼承可能看起來和基於類的集成很像,但它們其實是徹底不一樣的。在基於原型的語言中,對象的行爲經過一個對象做爲另外一個對象的原型來複用。在這樣的原型鏈中,當給定對象沒有指定屬性時,它會在其原型中查找,找不到就繼續這個流程,直到它找到原型屬性,或者找遍底層原型也沒找到爲止。
const arr = [];
const arrPrototype = Object.getPrototypeOf(arr);
arr.push(1) // .push() originates in arrPrototype
複製代碼
你可能想知道基於原型的繼承是否已經被 ES6 中基於類的繼承取代(ES6 引入了類),答案是否認的。ES6 類只是基於原型繼承概念的一個很好的語法糖。
咱們已經介紹了不少有趣的東西,但也只是剛剛觸及了皮毛而已。我剛纔提到的全部內容都是 ECMAScript 規範中的定義。但有趣的是,像事件循環甚至垃圾回收器這些都不在規範裏。ECMAScript 只關注 JS 自己,實現細節則留給其餘人解答(其餘人主要是瀏覽器廠商)。這就是爲何雖然全部 JS 引擎都遵循相同的規範,但它們管理內存的方式能夠不同,是否作 JIT 編譯也說不許,等等。那麼這一切意味着什麼呢?
咱們先來談談JIT。如前所述,將 JS 視爲一種解釋性語言是不對的。之前不少年 JS 的確是解釋性的,但最近出現了一些變化,這種假設也隨之過期了。許多流行的 JS 引擎爲了使 JS 執行更快,引入了一種稱爲 Just-In-Time 編譯的功能。簡而言之,這意味着 JS 代碼會在執行期間直接編譯成機器碼(至少 V8 是這樣作的),再也不有解釋這一步。這個流程耗時稍長,但輸出的結果性能更強。爲了在有限的時間內完成工做,V8 實際上有兩個編譯器(不算與WebAssembly 相關的內容)。其中一個是通用的,可以很是快地編譯任何 JS 代碼,但只輸出性能通常的結果;而另外一個編譯速度有點慢,是用來編譯經常使用代碼的,其輸出結果性能極高。固然,由於 JS 有動態類型的特性,這些編譯器也很差作。因此類型不變的狀況下第二個編譯器的效果最好,能讓你的代碼運行起來快得多。
但既然 JIT 這麼快的話,爲何 JS 一開始不用它呢?咱們也不太清楚,但我猜這是由於 JS 之前不須要那麼多的性能提高,並且標準解釋器更容易實現。在過去 JS 代碼通常也就那麼幾行,就算用了 JIT 也可能由於編譯開銷反而損失一些性能。但現在瀏覽器(以及許多其餘地方)使用的 JS 代碼數量顯著增長,JIT 編譯確定是走對了路。
在 JS 代碼的執行過程當中會分配兩個內存區域——調用棧和堆。第一個性能很是高,所以用於連續執行所提供的函數。每一個函數調用在調用棧中建立一個所謂的「框架」,其中包含其局部變量的副本和 this。你能夠經過 Chrome 調試器查看它。就像在其餘與堆棧相似的數據結構中同樣,調用棧的幀被推送或彈出堆棧,具體取決於正在執行或終止的新函數。你可能見過調用棧上限溢出錯誤,一般是因爲某種形式的無限循環致使的。
談到堆,就像現實生活中同樣,JS 堆是存儲本地範圍以外對象的地方。它比調用棧慢得多。這就是爲何訪問本地變量時速度可能會快不少。堆也是存放未被訪問或使用的對象的地方,這種對象就是垃圾。有垃圾就要有垃圾回收器。須要時 JS 運行時的垃圾回收器就會激活,清理堆並釋放內存。
如今咱們知道了調用棧和堆都是什麼意思,而後就能夠討論事件循環了。你可能知道 JS 是一種單線程語言。這也不是實際規範中的定義,屬於實現細節的範疇。回顧歷史,全部 JS 實現都是單線程的。你可能瞭解瀏覽器的 Web Worker或Node.js子進程 之類的東西——但它們並不能真正使 JS 自己變成多線程的。這兩個功能確實提供了多線程能力,但它們都不是 JS 自己的一部分,而分別是 Web API 和 Node.js 運行時。
那麼事件循環是如何工做的呢?其實很簡單,JS 從不真正等待函數的返回值,而是監聽傳入的事件。這樣一來,一旦 JS 檢測到新發出的事件(好比說用戶單擊),就會調用指定的回調。而後 JS 只會等待同步代碼完成執行,全部這些都在永無止境的非阻塞循環,也就是事件循環中重複。這是很是簡化的解釋,但做爲基礎知識來講足夠了。
對事件循環來講,須要注意的是同步和異步代碼不會被平等對待。相反,JS 首先執行同步代碼,而後檢查任務隊列是否須要執行任何異步操做。下面是示例:
setTimeout(() => console.log("Second"), 0);
console.log("First");
/* Console: > "First" > "Second" */
複製代碼
執行上面的代碼片斷時,你應該注意到雖然 setTimeout 排在第一位,而且它的超時時間是 0,它仍然會在同步代碼以後執行。
若是你接觸過異步代碼,可能也瞭解過Promise。這裏要注意一個小細節,Promise 有本身的特殊隊列——微任務隊列。這裏只要記住這個微任務隊列比一般的任務隊列優先級更高。所以若是在隊列中有任何 Promise 在等待,它將在任何其餘異步操做(如 setTimeout)以前運行:
setTimeout(() => console.log("Third"), 0);
Promise.resolve().then(() => console.log("Second"));
console.log("First");
/* Console: > "First" > "Second" > "Third" */
複製代碼
如你所見,就算是基礎內容也沒那麼簡單。不過這些內容理解起來應該仍是比較容易的,並且就算你不瞭解這些東西也能編寫出優秀的 JS 代碼。我認爲只有事件循環的內容是必須瞭解的部分。但知識固然是越多越好。