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