javascript
精读《你不知道的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 中的數字包括“整 數”和“浮點型”。
基本類型中定義了幾個特殊的值。
null 類型只有一個值 null , undefined 類型也只有一個值 undefined 。 所有變量在賦值之 前默認值都是 undefined 。 void 運算符返回 undefined 。
數 字 類 型 有 幾 個 特 殊 值, 包 括 NaN ( 意 指“not a number” , 更 確 切 地 說 是“invalid number” )、 Infinity 、 -Infinity 和 -0 。
簡單標量基本類型值(字符串和數字等)通過值復制來賦值 / 傳遞, 而復合值(對象等) 通過引用復制來賦值 / 傳遞。 JavaScript 中的引用和其他語言中的引用 / 指針不同,它們不 能指向別的變量 / 引用,只能指向值。
原生函數
JavaScript 為基本數據類型值提供了封裝對象,稱為原生函數(如 String 、 Number 、 Boolean 等)。它們為基本數據類型值提供了該子類型所特有的方法和屬性(如: String#trim() 和 Array#concat(..) )。
對于簡單標量基本類型值,比如 "abc" ,如果要訪問它的 length 屬性或 String.prototype 方法, 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 程序總是至少分為兩個塊:第一塊 現在 運行;下一塊 將來 運行, 以響 應某個事件。盡管程序是一塊一塊執行的,但是所有這些塊共享對程序作用域和狀態的訪 問,所以對狀態的修改都是在之前累積的修改之上進行的。一旦有事件需要運行, 事件循環就會運行, 直到隊列清空。 事件循環的每一輪稱為一個 tick。 用戶交互、IO 和定時器會向事件隊列中加入事件。
任意時刻,一次只能從隊列中處理一個事件。執行事件的時候,可能直接或間接地引發一 個或多個后續事件。
并發是指兩個或多個事件鏈隨時間發展交替執行,以至于從更高的層次來看,就像是同時 在運行(盡管在任意時刻只處理一個事件)。
通常需要對這些并發執行的“進程”(有別于操作系統中的進程概念)進行某種形式的交 互協調,比如需要確保執行順序或者需要防止競態出現。這些“進程”也可以通過把自身 分割為更小的塊,以便其他“進程”插入進來。
回調
回調函數是 JavaScript 異步的基本單元。但是隨著 JavaScript 越來越成熟,對于異步編程領域的發展,回調已經不夠用了。
第一,大腦對于事情的計劃方式是線性的、阻塞的、單線程的語義,但是回調表達異步流 程的方式是非線性的、非順序的,這使得正確推導這樣的代碼難度很大。難于理解的代碼 是壞代碼,會導致壞 bug。
我們需要一種更同步、更順序、更阻塞的的方式來表達異步,就像我們的大腦一樣。
第二,也是更重要的一點,回調會受到控制反轉的影響,因為回調暗中把控制權交給第三 方(通常是不受你控制的第三方工具!)來調用你代碼中的 continuation。 這種控制轉移導 致一系列麻煩的信任問題,比如回調被調用的次數是否會超出預期。
可以發明一些特定邏輯來解決這些信任問題,但是其難度高于應有的水平,可能會產生更 笨重、更難維護的代碼,并且缺少足夠的保護,其中的損害要直到你受到 bug 的影響才會 被發現。
我們需要一個通用的方案來解決這些信任問題。不管我們創建多少回調,這一方案都應可 以復用,且沒有重復代碼的開銷。
我們需要比回調更好的機制。到目前為止,回調提供了很好的服務,但是未來的 JavaScript 需要更高級、功能更強大的異步模式。本書接下來的幾章會深入探討這些新型技術。
Promise
Promise 非常好,請使用。它們解決了我們因只用回調的代碼而備受困擾的控制反轉問題。
它們并沒有擯棄回調,只是把回調的安排轉交給了一個位于我們和其他工具之間的可信任 的中介機制。
Promise 鏈也開始提供(盡管并不完美)以順序的方式表達異步流的一個更好的方法,這 有助于我們的大腦更好地計劃和維護異步 JavaScript 代碼。我們將在第 4 章看到針對這個 問題的一種更好的解決方案!
生成器
生成器是 ES6 的一個新的函數類型, 它并不像普通函數那樣總是運行到結束。 取而代之 的是, 生成器可以在運行當中(完全保持其狀態)暫停, 并且將來再從暫停的地方恢復 運行。這種交替的暫停和恢復是合作性的而不是搶占式的,這意味著生成器具有獨一無二的能力 來暫停自身,這是通過關鍵字 yield 實現的。不過,只有控制生成器的迭代器具有恢復生 成器的能力(通過 next(..) )。
yield / next(..) 這一對不只是一種控制機制,實際上也是一種雙向消息傳遞機制。 yield .. 表 達式本質上是暫停下來等待某個值,接下來的 next(..) 調用會向被暫停的 yield 表達式傳回 一個值(或者是隱式的 undefined )。
在異步控制流程方面,生成器的關鍵優點是:生成器內部的代碼是以自然的同步 / 順序方 式表達任務的一系列步驟。其技巧在于,我們把可能的異步隱藏在了關鍵字 yield 的后面, 把異步移動到控制生成器的迭代器的代碼部分。
換句話說,生成器為異步代碼保持了順序、同步、阻塞的代碼模式,這使得大腦可以更自 然地追蹤代碼,解決了基于回調的異步的兩個關鍵缺陷之一。
程序性能
本部分的前四章都是基于這樣一個前提:異步編碼模式使我們能夠編寫更高效的代碼,通 常能夠帶來非常大的改進。但是,異步特性只能讓你走這么遠,因為它本質上還是綁定在 一個單事件循環線程上。
因此,在這一章里,我們介紹了幾種能夠進一步提高性能的程序級別的機制。
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 在程序級別將獲得更加優化的性能。
性能測試與調優
對一段代碼進行有效的性能測試,特別是與同樣代碼的另外一個選擇對比來看看哪種方案 更快,需要認真注意細節。
與其打造你自己的統計有效的性能測試邏輯,不如直接使用 Benchmark.js 庫,它已經為你 實現了這些。但是,編寫測試要小心,因為我們很容易就會構造一個看似有效實際卻有缺 陷的測試,即使是微小的差異也可能扭曲結果,使其完全不可靠。
從盡可能多的環境中得到盡可能多的測試結果以消除硬件/ 設備的偏差, 這一點很重要。 jsPerf.com 是很好的網站,用于眾包性能測試運行。
遺憾的是,很多常用的性能測試執迷于無關緊要的微觀性能細節,比如 x 對比 x 。編 寫好的測試意味著理解如何關注大局, 比如關鍵路徑上的優化以及避免落入類似不同的 JavaScript 實現細節這樣的陷阱中。
尾調用優化是 ES6 要求的一種優化方法。它使 JavaScript 中原本不可能的一些遞歸模式變 得實際。 TCO 允許一個函數在結尾處調用另外一個函數來執行,不需要任何額外資源。這意味著,對遞歸算法來說,引擎不再需要限制棧深度。
擴展
思維導圖能比較清晰的還原整本書的知識結構體系,如果你還沒用看過這本書,可以按照這個思維導圖的思路快速預習一遍,提高學習效率。學習新事物總容易遺忘,我比較喜歡在看書的時候用思維導圖做些記錄,便于自己后期復習,如果你已經看過了這本書,也建議你收藏復習。如果你有神馬建議或則想法,歡迎留言或加我微信交流:646321933,備注技術交流
精讀《你不知道的javascript》上卷
精讀《深入淺出Node.js》
精讀《圖解HTTP》
思維導圖下載地址
你不知道的 javascript(中卷)PDF 下載地址
總結
以上是生活随笔為你收集整理的精读《你不知道的javascript》中卷的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 虾扯蛋之函数防抖和节流
- 下一篇: 撸个微信小程序的省市区选择器