精讀《你不知道的javascript》中卷

程式設計師解決師發表於2018-06-20

前言

《你不知道的 javascript》是一個前端學習必讀的系列,讓不求甚解的JavaScript開發者迎難而上,深入語言內部,弄清楚JavaScript每一個零部件的用途。本書《你不知道的javascript》中卷介紹了該系列的兩個主題:“型別和語法”以及“非同步與效能”。這兩塊也是值得我們反覆去學習琢磨的兩塊只是內容,今天我們用思維導圖的方式來精讀一遍。(思維導圖圖片可能有點小,記得點開看,你會有所收穫)

第一部分 作用域和閉包

型別

精讀《你不知道的javascript》中卷
JavaScript 有 七 種 內 置 類 型: null 、 undefined 、 boolean 、 number 、 string 、 object 和 symbol ,可以使用 typeof 運算子來檢視。

變數沒有型別,但它們持有的值有型別。型別定義了值的行為特徵。

很多開發人員將 undefined 和 undeclared 混 為 一 談, 但 在 JavaScript 中 它 們 是 兩 碼 事。 undefined 是值的一種。 undeclared 則表示變數還沒有被宣告過。

遺憾的是, JavaScript 卻將它們混為一談, 在我們試圖訪問 "undeclared" 變數時這樣報 錯:ReferenceError: a is not defined, 並 且 typeof 對 undefined 和 undeclared 變 量 都 返 回 "undefined" 。

然而,通過 typeof 的安全防範機制(阻止報錯)來檢查 undeclared 變數,有時是個不錯的辦法。

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

JavaScript 中的陣列是通過數字索引的一組任意型別的值。字串和陣列類似,但是它們的 行為特徵不同, 在將字元作為陣列來處理時需要特別小心。 JavaScript 中的數字包括“整 數”和“浮點型”。

基本型別中定義了幾個特殊的值。

null 型別只有一個值 null , undefined 型別也只有一個值 undefined 。 所有變數在賦值之 前預設值都是 undefined 。 void 運算子返回 undefined 。

數 字 類 型 有 幾 個 特 殊 值, 包 括 NaN ( 意 指“not a number” , 更 確 切 地 說 是“invalid number” )、 +Infinity 、 -Infinity 和 -0 。

簡單標量基本型別值(字串和數字等)通過值複製來賦值 / 傳遞, 而複合值(物件等) 通過引用複製來賦值 / 傳遞。 JavaScript 中的引用和其他語言中的引用 / 指標不同,它們不 能指向別的變數 / 引用,只能指向值。

原生函式

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

JavaScript 為基本資料型別值提供了封裝物件,稱為原生函式(如 String 、 Number 、 Boolean 等)。它們為基本資料型別值提供了該子型別所特有的方法和屬性(如: String#trim() 和 Array#concat(..) )。

對於簡單標量基本型別值,比如 "abc" ,如果要訪問它的 length 屬性或 String.prototype 方法, JavaScript 引擎會自動對該值進行封裝(即用相應型別的封裝物件來包裝它)來實現對這些屬性和方法的訪問。

強制型別轉換

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

本章介紹了 JavaScript 的資料型別之間的轉換,即強制型別轉換:包括顯式和隱式。

強制型別轉換常常為人詬病, 但實際上很多時候它們是非常有用的。 作為有使命感的 JavaScript 開發人員,我們有必要深入瞭解強制型別轉換,這樣就能取其精華,去其糟粕。

顯式強制型別轉換明確告訴我們哪裡發生了型別轉換, 有助於提高程式碼可讀性和可維 護性。

隱式強制型別轉換則沒有那麼明顯,是其他操作的副作用。感覺上好像是顯式強制型別轉 換的反面,實際上隱式強制型別轉換也有助於提高程式碼的可讀性。

在處理強制型別轉換的時候要十分小心,尤其是隱式強制型別轉換。在編碼的時候,要知 其然,還要知其所以然,並努力讓程式碼清晰易讀。

語法

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷
JavaScript 語法規則中的許多細節需要我們多花點時間和精力來了解。 從長遠來看, 這有 助於更深入地掌握這門語言。

語句和表示式在英語中都能找到類比——語句就像英語中的句子,而表示式就像短語。表 達式可以是簡單獨立的,否則可能會產生副作用。

JavaScript 語法規則之上是語義規則(也稱作上下文)。例如, { } 在不同情況下的意思不 盡相同,可以是語句塊、物件常量、解構賦值(ES6)或者命名函式引數(ES6)。

JavaScript 詳細定義了運算子的優先順序(運算子執行的先後順序)和關聯(多個運算子的 組合方式)。只要熟練掌握了這些規則,就能對如何合理地運用它們作出自己的判斷。

ASI(自動分號插入)是 JavaScript 引擎的程式碼解析糾錯機制, 它會在需要的地方自動插 入分號來糾正解析錯誤。問題在於這是否意味著大多數的分號都不是必要的(可以省略), 或者由於分號缺失導致的錯誤是否都可以交給 JavaScript 引擎來處理。

JavaScript 中有很多錯誤型別, 分為兩大類:早期錯誤(編譯時錯誤, 無法被捕獲)和運 行時錯誤(可以通過 try..catch 來捕獲)。所有語法錯誤都是早期錯誤,程式有語法錯誤 則無法執行。

函式引數和命名引數之間的關係非常微妙。尤其是 arguments 陣列,它的抽象洩漏給我們 挖了不少坑。因此,儘量不要使用 arguments ,如果非用不可,也切勿同時使用 arguments 和其對應的命名引數。

finally 中程式碼的處理順序需要特別注意。它們有時能派上很大用場,但也容易引起困惑, 特別是在和帶標籤的程式碼塊混用時。總之,使用 finally 旨在讓程式碼更加簡潔易讀,切忌 弄巧成拙。

switch 相對於 if..else if.. 來說更為簡潔。需要注意的一點是,如果對其理解得不夠透 徹,稍不注意就很容易出錯。

第二部分 this 和物件原型

非同步:現在與將來

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷
實際上, JavaScript 程式總是至少分為兩個塊:第一塊 現在 執行;下一塊 將來 執行, 以響 應某個事件。儘管程式是一塊一塊執行的,但是所有這些塊共享對程式作用域和狀態的訪 問,所以對狀態的修改都是在之前累積的修改之上進行的。

一旦有事件需要執行, 事件迴圈就會執行, 直到佇列清空。 事件迴圈的每一輪稱為一個 tick。 使用者互動、IO 和定時器會向事件佇列中加入事件。

任意時刻,一次只能從佇列中處理一個事件。執行事件的時候,可能直接或間接地引發一 個或多個後續事件。

併發是指兩個或多個事件鏈隨時間發展交替執行,以至於從更高的層次來看,就像是同時 在執行(儘管在任意時刻只處理一個事件)。

通常需要對這些併發執行的“程式”(有別於作業系統中的程式概念)進行某種形式的交 互協調,比如需要確保執行順序或者需要防止競態出現。這些“程式”也可以通過把自身 分割為更小的塊,以便其他“程式”插入進來。

回撥

精讀《你不知道的javascript》中卷

回撥函式是 JavaScript 非同步的基本單元。但是隨著 JavaScript 越來越成熟,對於非同步程式設計領域的發展,回撥已經不夠用了。

第一,大腦對於事情的計劃方式是線性的、阻塞的、單執行緒的語義,但是回撥錶達非同步流 程的方式是非線性的、非順序的,這使得正確推導這樣的程式碼難度很大。難於理解的程式碼 是壞程式碼,會導致壞 bug。

我們需要一種更同步、更順序、更阻塞的的方式來表達非同步,就像我們的大腦一樣。

第二,也是更重要的一點,回撥會受到控制反轉的影響,因為回撥暗中把控制權交給第三 方(通常是不受你控制的第三方工具!)來呼叫你程式碼中的 continuation。 這種控制轉移導 致一系列麻煩的信任問題,比如回撥被呼叫的次數是否會超出預期。

可以發明一些特定邏輯來解決這些信任問題,但是其難度高於應有的水平,可能會產生更 笨重、更難維護的程式碼,並且缺少足夠的保護,其中的損害要直到你受到 bug 的影響才會 被發現。

我們需要一個通用的方案來解決這些信任問題。不管我們建立多少回撥,這一方案都應可 以複用,且沒有重複程式碼的開銷。

我們需要比回撥更好的機制。到目前為止,回撥提供了很好的服務,但是未來的 JavaScript 需要更高階、功能更強大的非同步模式。本書接下來的幾章會深入探討這些新型技術。

Promise

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

Promise 非常好,請使用。它們解決了我們因只用回撥的程式碼而備受困擾的控制反轉問題。

它們並沒有擯棄回撥,只是把回撥的安排轉交給了一個位於我們和其他工具之間的可信任 的中介機制。

Promise 鏈也開始提供(儘管並不完美)以順序的方式表達非同步流的一個更好的方法,這 有助於我們的大腦更好地計劃和維護非同步 JavaScript 程式碼。我們將在第 4 章看到針對這個 問題的一種更好的解決方案!

生成器

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷
生成器是 ES6 的一個新的函式型別, 它並不像普通函式那樣總是執行到結束。 取而代之 的是, 生成器可以在執行當中(完全保持其狀態)暫停, 並且將來再從暫停的地方恢復 執行。

這種交替的暫停和恢復是合作性的而不是搶佔式的,這意味著生成器具有獨一無二的能力 來暫停自身,這是通過關鍵字 yield 實現的。不過,只有控制生成器的迭代器具有恢復生 成器的能力(通過 next(..) )。

yield / next(..) 這一對不只是一種控制機制,實際上也是一種雙向訊息傳遞機制。 yield .. 表 達式本質上是暫停下來等待某個值,接下來的 next(..) 呼叫會向被暫停的 yield 表示式傳回 一個值(或者是隱式的 undefined )。

在非同步控制流程方面,生成器的關鍵優點是:生成器內部的程式碼是以自然的同步 / 順序方 式表達任務的一系列步驟。其技巧在於,我們把可能的非同步隱藏在了關鍵字 yield 的後面, 把非同步移動到控制生成器的迭代器的程式碼部分。

換句話說,生成器為非同步程式碼保持了順序、同步、阻塞的程式碼模式,這使得大腦可以更自 然地追蹤程式碼,解決了基於回撥的非同步的兩個關鍵缺陷之一。

程式效能

精讀《你不知道的javascript》中卷

精讀《你不知道的javascript》中卷

本部分的前四章都是基於這樣一個前提:非同步編碼模式使我們能夠編寫更高效的程式碼,通 常能夠帶來非常大的改進。但是,非同步特性只能讓你走這麼遠,因為它本質上還是繫結在 一個單事件迴圈執行緒上。

因此,在這一章裡,我們介紹了幾種能夠進一步提高效能的程式級別的機制。

Web Worker 讓你可以在獨立的執行緒執行一個 JavaScript 檔案(即程式),使用非同步事件在 執行緒之間傳遞訊息。 它們非常適用於把長時間的或資源密集型的任務解除安裝到不同的執行緒 中,以提高主 UI 執行緒的響應性。

SIMD 打算把 CPU 級的並行數學運算對映到 JavaScript API, 以獲得高效能的資料並行運算,比如在大資料集上的數字處理。

最後, asm.js 描述了 JavaScript 的一個很小的子集, 它避免了 JavaScript 難以優化的部分 (比如垃圾收集和強制型別轉換),並且讓 JavaScript 引擎識別並通過激進的優化執行這樣 的程式碼。可以手工編寫 asm.js, 但是會極端費力且容易出錯,類似於手寫組合語言(這也 是其名字的由來)。實際上, asm.js 也是高度優化的程式語言交叉編譯的一個很好的目標, 比如 Emscripten 把 C/C++ 轉換成 JavaScript(https://github.com/kripken/emscripten/wiki) 。

JavaScript 還有一些更加激進的思路已經進入非常早期的討論, 儘管本章並沒有明確包含 這些內容,比如近似的直接多執行緒功能(而不是藏在資料結構 API 後面)。不管這些最終 會不會實現,還是我們將只能看到更多的並行特性偷偷加入 JavaScript, 但確實可以預見, 未來 JavaScript 在程式級別將獲得更加優化的效能。

效能測試與調優

精讀《你不知道的javascript》中卷

對一段程式碼進行有效的效能測試,特別是與同樣程式碼的另外一個選擇對比來看看哪種方案 更快,需要認真注意細節。

與其打造你自己的統計有效的效能測試邏輯,不如直接使用 Benchmark.js 庫,它已經為你 實現了這些。但是,編寫測試要小心,因為我們很容易就會構造一個看似有效實際卻有缺 陷的測試,即使是微小的差異也可能扭曲結果,使其完全不可靠。

從儘可能多的環境中得到儘可能多的測試結果以消除硬體/ 裝置的偏差, 這一點很重要。 jsPerf.com 是很好的網站,用於眾包效能測試執行。

遺憾的是,很多常用的效能測試執迷於無關緊要的微觀效能細節,比如 x++ 對比 ++x 。編 寫好的測試意味著理解如何關注大局, 比如關鍵路徑上的優化以及避免落入類似不同的 JavaScript 實現細節這樣的陷阱中。

尾呼叫優化是 ES6 要求的一種優化方法。它使 JavaScript 中原本不可能的一些遞迴模式變 得實際。 TCO 允許一個函式在結尾處呼叫另外一個函式來執行,不需要任何額外資源。這意味著,對遞迴演算法來說,引擎不再需要限制棧深度。

擴充套件

思維導圖能比較清晰的還原整本書的知識結構體系,如果你還沒用看過這本書,可以按照這個思維導圖的思路快速預習一遍,提高學習效率。學習新事物總容易遺忘,我比較喜歡在看書的時候用思維導圖做些記錄,便於自己後期複習,如果你已經看過了這本書,也建議你收藏複習。如果你有神馬建議或則想法,歡迎留言或加我微信交流:646321933,備註技術交流

精讀《你不知道的javascript》上卷

精讀《深入淺出Node.js》

精讀《圖解HTTP》

思維導圖下載地址

你不知道的 javascript(中卷)PDF 下載地址

相關文章