前言
《你不知道的 javascript》是一個前端學習必讀的系列,讓不求甚解的JavaScript開發者迎難而上,深入語言內部,弄清楚JavaScript每一個零部件的用途。本書介紹了該系列的兩個主題:“作用域和閉包”以及“this和物件原型”。這兩塊也是值得我們反覆去學習琢磨的兩塊只是內容,今天我們用思維導圖的方式來精讀一遍。(思維導圖圖片可能有點小,記得點開看,你會有所收穫)
第一部分 作用域和閉包
作用域是什麼
作用域是一套規則,用於確定在何處以及如何查詢變數(識別符號)。如果查詢的目的是對 變數進行賦值,那麼就會使用 LHS 查詢;
如果目的是獲取變數的值,就會使用 RHS 查詢。賦值操作符會導致 LHS 查詢。 的賦值操作。 =操作符或呼叫函式時傳入引數的操作都會導致關聯作用域的賦值操作。JavaScript 引擎首先會在程式碼執行前對其進行編譯,在這個過程中,像 var a = 2 這樣的聲 明會被分解成兩個獨立的步驟:
- 首先, var a 在其作用域中宣告新變數。這會在最開始的階段,也就是程式碼執行前進行。
- 接下來, a = 2 會查詢(LHS 查詢)變數 a 並對其進行賦值。
LHS 和 RHS 查詢都會在當前執行作用域中開始,如果有需要(也就是說它們沒有找到所 需的識別符號),就會向上級作用域繼續查詢目標識別符號,這樣每次上升一級作用域(一層 樓),最後抵達全域性作用域(頂層),無論找到或沒找到都將停止。
不成功的RHS引用會導致丟擲 ReferenceError 異常。不成功的 LHS 引用會導致自動隱式地建立一個全域性變數(非嚴格模式下),該變數使用 LHS 引用的目標作為識別符號,或者拋 出 ReferenceError 異常(嚴格模式下)。
詞法作用域
詞法作用域意味著作用域是由書寫程式碼時函式宣告的位置來決定的。編譯的詞法分析階段 基本能夠知道全部識別符號在哪裡以及是如何宣告的,從而能夠預測在執行過程中如何對它 們進行查詢。
JavaScript 中有兩個機制可以“欺騙”詞法作用域: eval(..) 和 with 。 前者可以對一段包 含一個或多個宣告的“程式碼”字串進行演算,並藉此來修改已經存在的詞法作用域(在 執行時)。後者本質上是通過將一個物件的引用 當作 作用域來處理,將物件的屬性當作作 用域中的識別符號來處理,從而建立了一個新的詞法作用域(同樣是在執行時)。
這兩個機制的副作用是引擎無法在編譯時對作用域查詢進行優化,因為引擎只能謹慎地認 為這樣的優化是無效的。使用這其中任何一個機制都 將 導致程式碼執行變慢。 不要使用它們。
函式作用域和塊作用域
函式是 JavaScript 中最常見的作用域單元。本質上,宣告在一個函式內部的變數或函式會 在所處的作用域中“隱藏”起來,這是有意為之的良好軟體的設計原則。
但函式不是唯一的作用域單元。塊作用域指的是變數和函式不僅可以屬於所處的作用域, 也可以屬於某個程式碼塊(通常指 {
..
} 內部)。
從 ES3 開始, try/catch 結構在 catch 分句中具有塊作用域。在 ES6 中引入了 let 關鍵字( var 關鍵字的表親), 用來在任意程式碼塊中宣告變數。 if(..) {
let a = 2;
} 會宣告一個劫持了 if 的 {
..
} 塊的變數,並且將變數新增到這個塊 中。
有些人認為塊作用域不應該完全作為函式作用域的替代方案。兩種功能應該同時存在,開 發者可以並且也應該根據需要選擇使用何種作用域,創造可讀、可維護的優良程式碼。
提升
我們習慣將 var a = 2;
看作一個宣告,而實際上 JavaScript 引擎並不這麼認為。它將 var a 和 a = 2 當作兩個單獨的宣告,第一個是編譯階段的任務,而第二個則是執行階段的任務。
這意味著無論作用域中的宣告出現在什麼地方,都將在程式碼本身被執行前 首先 進行處理。 可以將這個過程形象地想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。
宣告本身會被提升,而包括函式表示式的賦值在內的賦值操作並不會提升。
要注意避免重複宣告,特別是當普通的 var 宣告和函式宣告混合在一起的時候,否則會引 起很多危險的問題!
作用域閉包
閉包就好像從 JavaScript 中分離出來的一個充滿神祕色彩的未開化世界,只有最勇敢的人 才能夠到達那裡。但實際上它只是一個標準,顯然就是關於如何在函式作為值按需傳遞的 詞法環境中書寫程式碼的。
當函式可以記住並訪問所在的詞法作用域,即使函式是在當前詞法作用域之外執行,這時 就產生了閉包。
如果沒能認出閉包,也不瞭解它的工作原理,在使用它的過程中就很容易犯錯,比如在循 環中。但同時閉包也是一個非常強大的工具,可以用多種形式來實現 模組 等模式。模組有兩個主要特徵:
(1)為建立內部作用域而呼叫了一個包裝函式;
(2)包裝函式的返回 值必須至少包括一個對內部函式的引用,這樣就會建立涵蓋整個包裝函式內部作用域的閉 包。
現在我們會發現程式碼中到處都有閉包存在,並且我們能夠識別閉包然後用它來做一些有用 的事!
第二部分 this 和物件原型
this 全面解析
如果要判斷一個執行中函式的 this 繫結,就需要找到這個函式的直接呼叫位置。找到之後 就可以順序應用下面這四條規則來判斷 this 的繫結物件。
-
由 new 呼叫?繫結到新建立的物件。
-
由 call 或者 apply (或者 bind )呼叫?繫結到指定的物件。
-
由上下文物件呼叫?繫結到那個上下文物件。
-
預設:在嚴格模式下繫結到 undefined ,否則繫結到全域性物件。
一定要注意,有些呼叫可能在無意中使用預設繫結規則。如果想“更安全”地忽略 this 綁 定,你可以使用一個 DMZ 物件,比如 ø = Object.create(null) ,以保護全域性物件。ES6中的箭頭函式並不會使用四條標準的繫結規則, 而是根據當前的詞法作用域來決定 this ,具體來說,箭頭函式會繼承外層函式呼叫的 this 繫結(無論 this 繫結到什麼)。這 其實和 ES6 之前程式碼中的 self = this 機制一樣。
物件
JavaScript 中的物件有字面形式(比如 var a = {
..
} )和構造形式(比如 var a = new Array(..) )。字面形式更常用,不過有時候構造形式可以提供更多選項。
許多人都以為“JavaScript 中萬物都是物件”,這是錯誤的。物件是 6 個(或者是 7 個,取 決於你的觀點)基礎型別之一。物件有包括 function 在內的子型別,不同子型別具有不同 的行為,比如內部標籤 [object Array] 表示這是物件的子型別陣列。
物件就是鍵 / 值對的集合。可以通過 .propName 或者 [“propName”] 語法來獲取屬性值。訪 問屬性時, 引擎實際上會呼叫內部的預設 [[Get]] 操作(在設定屬性值時是 [[Put]] ), [[Get]] 操作會檢查物件本身是否包含這個屬性,如果沒找到的話還會查詢 [[Prototype]] 鏈(參見第 5 章)。
屬性的特性可以通過屬性描述符來控制,比如 writable 和 configurable 。此外,可以使用 Object.preventExtensions(..) 、 Object.seal(..) 和 Object.freeze(..) 來設定物件(及其 屬性)的不可變性級別。
屬性不一定包含值——它們可能是具備 getter/setter 的“訪問描述符”。此外,屬性可以是 可列舉或者不可列舉的,這決定了它們是否會出現在 for..in 迴圈中。
你可以使用 ES6 的 for..of 語法來遍歷資料結構(陣列、物件, 等等)中的值, for..of 會尋找內建或者自定義的 @@iterator 物件並呼叫它的 next() 方法來遍歷資料值。
混合物件”類”
類是一種設計模式。 許多語言提供了對於面向類軟體設計的原生語法。 JavaScript 也有類 似的語法,但是和其他語言中的類完全不同。
類意味著複製。
傳統的類被例項化時,它的行為會被複制到例項中。類被繼承時,行為也會被複制到子類 中。
多型(在繼承鏈的不同層次名稱相同但是功能不同的函式)看起來似乎是從子類引用父 類,但是本質上引用的其實是複製的結果。
JavaScript 並不會(像類那樣)自動建立物件的副本。
混入模式(無論顯式還是隱式)可以用來模擬類的複製行為,但是通常會產生醜陋並且脆 弱的語法,比如顯式偽多型( OtherObj.methodName.call(this, …) ),這會讓程式碼更加難 懂並且難以維護。
此外, 顯式混入實際上無法完全模擬類的複製行為, 因為物件(和函式!別忘了函式也 是物件)只能複製引用, 無法複製被引用的物件或者函式本身。 忽視這一點會導致許多 問題。
總地來說,在 JavaScript 中模擬類是得不償失的,雖然能解決當前的問題,但是可能會埋下更多的隱患。
原型
如果要訪問物件中並不存在的一個屬性, [[Get]] 操作(參見第 3 章)就會查詢物件內部 [[Prototype]] 關聯的物件。這個關聯關係實際上定義了一條“原型鏈”(有點像巢狀的作用域鏈),在查詢屬性時會對它進行遍歷。
所有普通物件都有內建的 Object.prototype ,指向原型鏈的頂端(比如說全域性作用域),如 果在原型鏈中找不到指定的屬性就會停止。 toString() 、 valueOf() 和其他一些通用的功能 都存在於 Object.prototype 物件上,因此語言中所有的物件都可以使用它們。
關聯兩個物件最常用的方法是使用 new 關鍵詞進行函式呼叫, 在呼叫的 章)中會建立一個關聯其他物件的新物件。4個步驟(第2章)中會建立一個關聯其他物件的新物件。
使用 new 呼叫函式時會把新物件的 .prototype 屬性關聯到“其他物件”。帶 new 的函式呼叫 通常被稱為“建構函式呼叫”,儘管它們實際上和傳統面向類語言中的 類建構函式 不一樣。
JavaScript 是 中的機制有一個核心區別, 那就是不會進行復制, 物件之間是通過內部的
雖然這些 機制和傳統面向類語言中的“類初始化”和“類繼承”很相似, 但是 javascript 機制和傳統物件導向類語言中的“類初始化”和“類繼承”很相似但是 javascript 中的機制有一個核心區別,就是不會進行復制,物件之間是通過內部的 [[Prototype]] 鏈關聯的。
出於各種原因,以“繼承”結尾的術語(包括“原型繼承”)和其他物件導向的術語都無 法幫助你理解 JavaScript 的 真實 機制(不僅僅是限制我們的思維模式)。
相比之下,“委託”是一個更合適的術語,因為物件之間的關係不是 複製 而是委託。
行為委託
在軟體架構中你可以 選擇是否 使用類和繼承設計模式。大多數開發者理所當然地認為類是 唯一(合適)的程式碼組織方式,但是本章中我們看到了另一種更少見但是更強大的設計模式: 行為委託 。
行為委託認為物件之間是兄弟關係, 互相委託, 而不是父類和子類的關係。 JavaScript 的 [[Prototype]] 機制本質上就是行為委託機制。也就是說,我們可以選擇在 JavaScript 中努 力實現類機制(參見第 4 和第 5 章),也可以擁抱更自然的 [[Prototype]] 委託機制。
當你只用物件來設計程式碼時,不僅可以讓語法更加簡潔,而且可以讓程式碼結構更加清晰。
物件關聯(物件之前互相關聯)是一種編碼風格,它倡導的是直接建立和關聯物件,不把 它們抽象成類。物件關聯可以用基於 [[Prototype]] 的行為委託非常自然地實現。
擴充套件
思維導圖能比較清晰的還原整本書的知識結構體系,如果你還沒用看過這本書,可以按照這個思維導圖的思路快速預習一遍,提高學習效率。學習新事物總容易遺忘,我比較喜歡在看書的時候用思維導圖做些記錄,便於自己後期複習,如果你已經看過了這本書,也建議你收藏複習。如果你有神馬建議或則想法,歡迎留言或加我微信交流:646321933