面試前必備的 JavaScript 基礎(chǔ)知識(shí)梳理總結(jié)
1. JavaScript簡介
JavaScript 最開始是專門為瀏覽器設(shè)計(jì)的一門語言,但是現(xiàn)在也被用于很多其他的環(huán)境。
如今,JavaScript 已經(jīng)成為了與 HTML/CSS 完全集成的,使用最廣泛的瀏覽器語言。
有很多其他的語言可以被“編譯”成 JavaScript,這些語言還提供了更多的功能。建議最好了解一下這些語言,至少在掌握了 JavaScript 之后大致的了解一下。
2. 變量
我們可以使用 var、let 或 const 聲明變量來存儲(chǔ)數(shù)據(jù)。
let — 現(xiàn)代的變量聲明方式。
var — 老舊的變量聲明方式。一般情況下,我們不會(huì)再使用它。但是,我們會(huì)在 舊時(shí)的 "var" 章節(jié)介紹 var 和 let 的微妙差別,以防你需要它們。
const — 類似于 let,但是變量的值無法被修改。
變量應(yīng)當(dāng)以一種容易理解變量內(nèi)部是什么的方式進(jìn)行命名。
3. 數(shù)據(jù)類型
JavaScript 中有八種基本的數(shù)據(jù)類型(譯注:前七種為基本數(shù)據(jù)類型,也稱為原始類型,而 object 為復(fù)雜數(shù)據(jù)類型)。
number 用于任何類型的數(shù)字:整數(shù)或浮點(diǎn)數(shù),在 ±(253-1) 范圍內(nèi)的整數(shù)。
bigint 用于任意長度的整數(shù)。
string 用于字符串:一個(gè)字符串可以包含 0 個(gè)或多個(gè)字符,所以沒有單獨(dú)的單字符類型。
boolean 用于 true 和 false。
null 用于未知的值 —— 只有一個(gè) null 值的獨(dú)立類型。
undefined 用于未定義的值 —— 只有一個(gè) undefined 值的獨(dú)立類型。
symbol 用于唯一的標(biāo)識(shí)符。
object 用于更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
我們可以通過 typeof 運(yùn)算符查看存儲(chǔ)在變量中的數(shù)據(jù)類型。
兩種形式:typeof x 或者 typeof(x)。
以字符串的形式返回類型名稱,例如 "string"。
typeof null 會(huì)返回 "object" —— 這是 JavaScript 編程語言的一個(gè)錯(cuò)誤,實(shí)際上它并不是一個(gè) object。
4. 類型轉(zhuǎn)換
有三種常用的類型轉(zhuǎn)換:轉(zhuǎn)換為 string 類型、轉(zhuǎn)換為 number 類型和轉(zhuǎn)換為 boolean 類型。
字符串轉(zhuǎn)換 —— 轉(zhuǎn)換發(fā)生在輸出內(nèi)容的時(shí)候,也可以通過 String(value) 進(jìn)行顯式轉(zhuǎn)換。原始類型值的 string 類型轉(zhuǎn)換通常是很明顯的。
數(shù)字型轉(zhuǎn)換 —— 轉(zhuǎn)換發(fā)生在進(jìn)行算術(shù)操作時(shí),也可以通過 Number(value) 進(jìn)行顯式轉(zhuǎn)換。
數(shù)字型轉(zhuǎn)換遵循以下規(guī)則:

布爾型轉(zhuǎn)換遵循以下規(guī)則:布爾型轉(zhuǎn)換 —— 轉(zhuǎn)換發(fā)生在進(jìn)行邏輯操作時(shí),也可以通過 Boolean(value) 進(jìn)行顯式轉(zhuǎn)換。

對(duì) undefined 進(jìn)行數(shù)字型轉(zhuǎn)換時(shí),輸出結(jié)果為 NaN,而非 0。上述的大多數(shù)規(guī)則都容易理解和記憶。人們通常會(huì)犯錯(cuò)誤的值得注意的例子有以下幾個(gè):
對(duì) "0" 和只有空格的字符串(比如:" ")進(jìn)行布爾型轉(zhuǎn)換時(shí),輸出結(jié)果為 true。
5. 值的比較
比較運(yùn)算符始終返回布爾值。
字符串的比較,會(huì)按照“詞典”順序逐字符地比較大小。
當(dāng)對(duì)不同類型的值進(jìn)行比較時(shí),它們會(huì)先被轉(zhuǎn)化為數(shù)字(不包括嚴(yán)格相等檢查)再進(jìn)行比較。
在非嚴(yán)格相等 == 下,null 和 undefined 相等且各自不等于任何其他的值。
在使用 > 或 < 進(jìn)行比較時(shí),需要注意變量可能為 null/undefined 的情況。比較好的方法是單獨(dú)檢查變量是否等于 null/undefined。
6. 空值合并運(yùn)算符 '??'
空值合并運(yùn)算符 ?? 提供了一種從列表中選擇第一個(gè)“已定義的”值的簡便方式。
它被用于為變量分配默認(rèn)值:
// 當(dāng) height 的值為 null 或 undefined 時(shí),將 height 的值設(shè)置為 100
height = height ?? 100;
復(fù)制代碼
?? 運(yùn)算符的優(yōu)先級(jí)非常低,僅略高于 ? 和 =,因此在表達(dá)式中使用它時(shí)請(qǐng)考慮添加括號(hào)。
如果沒有明確添加括號(hào),不能將其與 || 或 && 一起使用。
7. 循環(huán):while 和 for
我們學(xué)習(xí)了三種循環(huán):
while —— 每次迭代之前都要檢查條件。
do..while —— 每次迭代后都要檢查條件。
for (;;) —— 每次迭代之前都要檢查條件,可以使用其他設(shè)置。
通常使用 while(true) 來構(gòu)造“無限”循環(huán)。這樣的循環(huán)和其他循環(huán)一樣,都可以通過 break 指令來終止。
如果我們不想在當(dāng)前迭代中做任何事,并且想要轉(zhuǎn)移至下一次迭代,那么可以使用 continue 指令。
break/continue 支持循環(huán)前的標(biāo)簽。標(biāo)簽是 break/continue 跳出嵌套循環(huán)以轉(zhuǎn)到外部的唯一方法。
8. 函數(shù)
函數(shù)聲明方式如下所示:
function name(parameters, delimited, by, comma) {
/* code */
}
復(fù)制代碼
作為參數(shù)傳遞給函數(shù)的值,會(huì)被復(fù)制到函數(shù)的局部變量。
函數(shù)可以訪問外部變量。但它只能從內(nèi)到外起作用。函數(shù)外部的代碼看不到函數(shù)內(nèi)的局部變量。
函數(shù)可以返回值。如果沒有返回值,則其返回的結(jié)果是 undefined。
為了使代碼簡潔易懂,建議在函數(shù)中主要使用局部變量和參數(shù),而不是外部變量。
與不獲取參數(shù)但將修改外部變量作為副作用的函數(shù)相比,獲取參數(shù)、使用參數(shù)并返回結(jié)果的函數(shù)更容易理解。
函數(shù)命名:
函數(shù)名應(yīng)該清楚地描述函數(shù)的功能。當(dāng)我們?cè)诖a中看到一個(gè)函數(shù)調(diào)用時(shí),一個(gè)好的函數(shù)名能夠讓我們馬上知道這個(gè)函數(shù)的功能是什么,會(huì)返回什么。
一個(gè)函數(shù)是一個(gè)行為,所以函數(shù)名通常是動(dòng)詞。
目前有許多優(yōu)秀的函數(shù)名前綴,如 create…、show…、get…、check… 等等。使用它們來提示函數(shù)的作用吧。
9. 函數(shù)表達(dá)式
函數(shù)是值。它們可以在代碼的任何地方被分配,復(fù)制或聲明。
如果函數(shù)在主代碼流中被聲明為單獨(dú)的語句,則稱為“函數(shù)聲明”。
如果該函數(shù)是作為表達(dá)式的一部分創(chuàng)建的,則稱其“函數(shù)表達(dá)式”。
在執(zhí)行代碼塊之前,內(nèi)部算法會(huì)先處理函數(shù)聲明。所以函數(shù)聲明在其被聲明的代碼塊內(nèi)的任何位置都是可見的。
函數(shù)表達(dá)式在執(zhí)行流程到達(dá)時(shí)創(chuàng)建。
在大多數(shù)情況下,當(dāng)我們需要聲明一個(gè)函數(shù)時(shí),最好使用函數(shù)聲明,因?yàn)楹瘮?shù)在被聲明之前也是可見的。這使我們?cè)诖a組織方面更具靈活性,通常也會(huì)使得代碼可讀性更高。
所以,僅當(dāng)函數(shù)聲明不適合對(duì)應(yīng)的任務(wù)時(shí),才應(yīng)使用函數(shù)表達(dá)式。
10. 箭頭函數(shù),基礎(chǔ)知識(shí)
對(duì)于一行代碼的函數(shù)來說,箭頭函數(shù)是相當(dāng)方便的。它具體有兩種:
不帶花括號(hào):(...args) => expression — 右側(cè)是一個(gè)表達(dá)式:函數(shù)計(jì)算表達(dá)式并返回其結(jié)果。
帶花括號(hào):(...args) => { body } — 花括號(hào)允許我們?cè)诤瘮?shù)中編寫多個(gè)語句,但是我們需要顯式地 return 來返回一些內(nèi)容。
11. 對(duì)象
對(duì)象是具有一些特殊特性的關(guān)聯(lián)數(shù)組。
它們存儲(chǔ)屬性(鍵值對(duì)),其中:
屬性的鍵必須是字符串或者 symbol(通常是字符串)。
值可以是任何類型。
我們可以用下面的方法訪問屬性:
點(diǎn)符號(hào): obj.property。
方括號(hào) obj["property"],方括號(hào)允許從變量中獲取鍵,例如 obj[varWithKey]。
其他操作:
刪除屬性:delete obj.prop。
檢查是否存在給定鍵的屬性:"key" in obj。
遍歷對(duì)象:for(let key in obj) 循環(huán)。
我們?cè)谶@一章學(xué)習(xí)的叫做“普通對(duì)象(plain object)”,或者就叫對(duì)象。
JavaScript 中還有很多其他類型的對(duì)象:
Array 用于存儲(chǔ)有序數(shù)據(jù)集合,
Date 用于存儲(chǔ)時(shí)間日期,
Error 用于存儲(chǔ)錯(cuò)誤信息。
……等等。
它們有著各自特別的特性,我們將在后面學(xué)習(xí)到。有時(shí)候大家會(huì)說“Array 類型”或“Date 類型”,但其實(shí)它們并不是自身所屬的類型,而是屬于一個(gè)對(duì)象類型即 “object”。它們以不同的方式對(duì) “object” 做了一些擴(kuò)展。
12. 對(duì)象引用和復(fù)制
對(duì)象通過引用被賦值和拷貝。換句話說,一個(gè)變量存儲(chǔ)的不是“對(duì)象的值”,而是一個(gè)對(duì)值的“引用”(內(nèi)存地址)。因此,拷貝此類變量或?qū)⑵渥鳛楹瘮?shù)參數(shù)傳遞時(shí),所拷貝的是引用,而不是對(duì)象本身。
所有通過被拷貝的引用的操作(如添加、刪除屬性)都作用在同一個(gè)對(duì)象上。
為了創(chuàng)建“真正的拷貝”(一個(gè)克隆),我們可以使用 Object.assign 來做所謂的“淺拷貝”(嵌套對(duì)象被通過引用進(jìn)行拷貝)或者使用“深拷貝”函數(shù),例如 \_.cloneDeep\(obj\)[1]。
13. 對(duì)象方法,"this"
存儲(chǔ)在對(duì)象屬性中的函數(shù)被稱為“方法”。
方法允許對(duì)象進(jìn)行像 object.doSomething() 這樣的“操作”。
方法可以將對(duì)象引用為 this。
this 的值是在程序運(yùn)行時(shí)得到的。
一個(gè)函數(shù)在聲明時(shí),可能就使用了 this,但是這個(gè) this 只有在函數(shù)被調(diào)用時(shí)才會(huì)有值。
可以在對(duì)象之間復(fù)制函數(shù)。
以“方法”的語法調(diào)用函數(shù)時(shí):object.method(),調(diào)用過程中的 this 值是 object。
請(qǐng)注意箭頭函數(shù)有些特別:它們沒有 this。在箭頭函數(shù)內(nèi)部訪問到的 this 都是從外部獲取的。
14. 可選鏈 "?."
可選鏈 ?. 語法有三種形式:
obj?.prop —— 如果 obj 存在則返回 obj.prop,否則返回 undefined。
obj?.[prop] —— 如果 obj 存在則返回 obj[prop],否則返回 undefined。
obj.method?.() —— 如果 obj.method 存在則調(diào)用 obj.method(),否則返回 undefined。
正如我們所看到的,這些語法形式用起來都很簡單直接。?. 檢查左邊部分是否為 null/undefined,如果不是則繼續(xù)運(yùn)算。
?. 鏈?zhǔn)刮覀兡軌虬踩卦L問嵌套屬性。
但是,我們應(yīng)該謹(jǐn)慎地使用 ?.,僅在當(dāng)左邊部分不存在也沒問題的情況下使用為宜。以保證在代碼中有編程上的錯(cuò)誤出現(xiàn)時(shí),也不會(huì)對(duì)我們隱藏。
15. Symbol 類型
Symbol 是唯一標(biāo)識(shí)符的基本類型
Symbol 是使用帶有可選描述(name)的 Symbol() 調(diào)用創(chuàng)建的。
Symbol 總是不同的值,即使它們有相同的名字。如果我們希望同名的 Symbol 相等,那么我們應(yīng)該使用全局注冊(cè)表:Symbol.for(key) 返回(如果需要的話則創(chuàng)建)一個(gè)以 key 作為名字的全局 Symbol。使用 Symbol.for 多次調(diào)用 key 相同的 Symbol 時(shí),返回的就是同一個(gè) Symbol。
Symbol 有兩個(gè)主要的使用場(chǎng)景:
“隱藏” 對(duì)象屬性。如果我們想要向“屬于”另一個(gè)腳本或者庫的對(duì)象添加一個(gè)屬性,我們可以創(chuàng)建一個(gè) Symbol 并使用它作為屬性的鍵。Symbol 屬性不會(huì)出現(xiàn)在 for..in 中,因此它不會(huì)意外地被與其他屬性一起處理。并且,它不會(huì)被直接訪問,因?yàn)榱硪粋€(gè)腳本沒有我們的 symbol。因此,該屬性將受到保護(hù),防止被意外使用或重寫。
因此我們可以使用 Symbol 屬性“秘密地”將一些東西隱藏到我們需要的對(duì)象中,但其他地方看不到它。
JavaScript 使用了許多系統(tǒng) Symbol,這些 Symbol 可以作為 Symbol.* 訪問。我們可以使用它們來改變一些內(nèi)置行為。例如,在本教程的后面部分,我們將使用 Symbol.iterator 來進(jìn)行 迭代[2] 操作,使用 Symbol.toPrimitive 來設(shè)置 對(duì)象原始值的轉(zhuǎn)換[3] 等等。
從技術(shù)上說,Symbol 不是 100% 隱藏的。有一個(gè)內(nèi)置方法 Object.getOwnPropertySymbols\(obj\)[4] 允許我們獲取所有的 Symbol。還有一個(gè)名為 Reflect.ownKeys\(obj\)[5] 的方法可以返回一個(gè)對(duì)象的 所有 鍵,包括 Symbol。所以它們并不是真正的隱藏。但是大多數(shù)庫、內(nèi)置方法和語法結(jié)構(gòu)都沒有使用這些方法。
16. 數(shù)字類型
要寫有很多零的數(shù)字:
將 "e" 和 0 的數(shù)量附加到數(shù)字后。就像:123e6 與 123 后面接 6 個(gè) 0 相同。
"e" 后面的負(fù)數(shù)將使數(shù)字除以 1 后面接著給定數(shù)量的零的數(shù)字。例如 123e-6 表示 0.000123(123 的百萬分之一)。
對(duì)于不同的數(shù)字系統(tǒng):
可以直接在十六進(jìn)制(0x),八進(jìn)制(0o)和二進(jìn)制(0b)系統(tǒng)中寫入數(shù)字。
parseInt(str,base) 將字符串 str 解析為在給定的 base 數(shù)字系統(tǒng)中的整數(shù),2 ≤ base ≤ 36。
num.toString(base) 將數(shù)字轉(zhuǎn)換為在給定的 base 數(shù)字系統(tǒng)中的字符串。
要將 12pt 和 100px 之類的值轉(zhuǎn)換為數(shù)字:
使用 parseInt/parseFloat 進(jìn)行“軟”轉(zhuǎn)換,它從字符串中讀取數(shù)字,然后返回在發(fā)生 error 前可以讀取到的值。
小數(shù):
使用 Math.floor,Math.ceil,Math.trunc,Math.round 或 num.toFixed(precision) 進(jìn)行舍入。
請(qǐng)確保記住使用小數(shù)時(shí)會(huì)損失精度。
更多數(shù)學(xué)函數(shù):
需要時(shí)請(qǐng)查看 Math[6] 對(duì)象。這個(gè)庫很小,但是可以滿足基本的需求。
17. 字符串
有 3 種類型的引號(hào)。反引號(hào)允許字符串跨越多行并可以使用 ${…} 在字符串中嵌入表達(dá)式。
JavaScript 中的字符串使用的是 UTF-16 編碼。
我們可以使用像 \n 這樣的特殊字符或通過使用 \u... 來操作它們的 unicode 進(jìn)行字符插入。
獲取字符時(shí),使用 []。
獲取子字符串,使用 slice 或 substring。
字符串的大/小寫轉(zhuǎn)換,使用:toLowerCase/toUpperCase。
查找子字符串時(shí),使用 indexOf 或 includes/startsWith/endsWith 進(jìn)行簡單檢查。
根據(jù)語言比較字符串時(shí)使用 localeCompare,否則將按字符代碼進(jìn)行比較。
還有其他幾種有用的字符串方法:
str.trim() —— 刪除字符串前后的空格 (“trims”)。
str.repeat(n) —— 重復(fù)字符串 n 次。
……更多內(nèi)容細(xì)節(jié)請(qǐng)參見 手冊(cè)[7]。
18. 數(shù)組
數(shù)組是一種特殊的對(duì)象,適用于存儲(chǔ)和管理有序的數(shù)據(jù)項(xiàng)。
聲明:
// 方括號(hào) (常見用法) let arr = [item1, item2...];
// new Array (極其少見) let arr = new Array(item1, item2...);
調(diào)用 new Array(number) 會(huì)創(chuàng)建一個(gè)給定長度的數(shù)組,但不含有任何項(xiàng)。
length 屬性是數(shù)組的長度,準(zhǔn)確地說,它是數(shù)組最后一個(gè)數(shù)字索引值加一。它由數(shù)組方法自動(dòng)調(diào)整。
如果我們手動(dòng)縮短 length,那么數(shù)組就會(huì)被截?cái)唷?/p>
我們可以通過下列操作以雙端隊(duì)列的方式使用數(shù)組:
push(...items) 在末端添加 items 項(xiàng)。
pop() 從末端移除并返回該元素。
shift() 從首端移除并返回該元素。
unshift(...items) 從首端添加 items 項(xiàng)。
遍歷數(shù)組的元素:
for (let i=0; i<arr.length; i++) — 運(yùn)行得最快,可兼容舊版本瀏覽器。
for (let item of arr) — 現(xiàn)代語法,只能訪問 items。
for (let i in arr) — 永遠(yuǎn)不要用這個(gè)。
比較數(shù)組時(shí),不要使用 == 運(yùn)算符(當(dāng)然也不要使用 > 和 < 等運(yùn)算符),因?yàn)樗鼈儾粫?huì)對(duì)數(shù)組進(jìn)行特殊處理。它們通常會(huì)像處理任意對(duì)象那樣處理數(shù)組,這通常不是我們想要的。
但是,我們可以使用 for..of 循環(huán)來逐項(xiàng)比較數(shù)組。
19. 數(shù)組方法
數(shù)組方法備忘單:
添加/刪除元素:
push(...items) —— 向尾端添加元素,
pop() —— 從尾端提取一個(gè)元素,
shift() —— 從首端提取一個(gè)元素,
unshift(...items) —— 向首端添加元素,
splice(pos, deleteCount, ...items) —— 從 pos 開始刪除 deleteCount 個(gè)元素,并插入 items。
slice(start, end) —— 創(chuàng)建一個(gè)新數(shù)組,將從索引 start 到索引 end(但不包括 end)的元素復(fù)制進(jìn)去。
concat(...items) —— 返回一個(gè)新數(shù)組:復(fù)制當(dāng)前數(shù)組的所有元素,并向其中添加 items。如果 items 中的任意一項(xiàng)是一個(gè)數(shù)組,那么就取其元素。
搜索元素:
indexOf/lastIndexOf(item, pos) —— 從索引 pos 開始搜索 item,搜索到則返回該項(xiàng)的索引,否則返回 -1。
includes(value) —— 如果數(shù)組有 value,則返回 true,否則返回 false。
find/filter(func) —— 通過 func 過濾元素,返回使 func 返回 true 的第一個(gè)值/所有值。
findIndex 和 find 類似,但返回索引而不是值。
遍歷元素:
forEach(func) —— 對(duì)每個(gè)元素都調(diào)用 func,不返回任何內(nèi)容。 轉(zhuǎn)換數(shù)組:
map(func) —— 根據(jù)對(duì)每個(gè)元素調(diào)用 func 的結(jié)果創(chuàng)建一個(gè)新數(shù)組。
sort(func) —— 對(duì)數(shù)組進(jìn)行原位(in-place)排序,然后返回它。
reverse() —— 原位(in-place)反轉(zhuǎn)數(shù)組,然后返回它。
split/join —— 將字符串轉(zhuǎn)換為數(shù)組并返回。
reduce/reduceRight(func, initial) —— 通過對(duì)每個(gè)元素調(diào)用 func 計(jì)算數(shù)組上的單個(gè)值,并在調(diào)用之間傳遞中間結(jié)果。
其他:
Array.isArray(arr) 檢查 arr 是否是一個(gè)數(shù)組。
請(qǐng)注意,sort,reverse 和 splice 方法修改的是數(shù)組本身。
這些是最常用的方法,它們覆蓋 99% 的用例。但是還有其他幾個(gè):
arr.some\(fn\)[8]/arr.every\(fn\)[9] 檢查數(shù)組。
與 map 類似,對(duì)數(shù)組的每個(gè)元素調(diào)用函數(shù) fn。如果任何/所有結(jié)果為 true,則返回 true,否則返回 false。
這兩個(gè)方法的行為類似于 || 和 && 運(yùn)算符:如果 fn 返回一個(gè)真值,arr.some() 立即返回 true 并停止迭代其余數(shù)組項(xiàng);如果 fn 返回一個(gè)假值,arr.every() 立即返回 false 并停止對(duì)其余數(shù)組項(xiàng)的迭代。
我們可以使用 every 來比較數(shù)組:
function arraysEqual(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}
alert( arraysEqual([1, 2], [1, 2])); // true
復(fù)制代碼
arr.fill\(value, start, end\)[10] —— 從索引 start 到 end,用重復(fù)的 value 填充數(shù)組。
arr.copyWithin\(target, start, end\)[11] —— 將從位置 start 到 end 的所有元素復(fù)制到 自身 的 target 位置(覆蓋現(xiàn)有元素)。
arr.flat\(depth\)[12]/arr.flatMap\(fn\)[13] 從多維數(shù)組創(chuàng)建一個(gè)新的扁平數(shù)組。
Array.of\(element0\[, element1\[, …\[, elementN\]\]\]\)[14] 基于可變數(shù)量的參數(shù)創(chuàng)建一個(gè)新的 Array 實(shí)例,而不需要考慮參數(shù)的數(shù)量或類型。
有關(guān)完整列表,請(qǐng)參閱 手冊(cè)[15]。
20. Iterable object(可迭代對(duì)象)
可以應(yīng)用 for..of 的對(duì)象被稱為 可迭代的。
技術(shù)上來說,可迭代對(duì)象必須實(shí)現(xiàn) Symbol.iterator 方法。
obj[Symbol.iterator]() 的結(jié)果被稱為 迭代器(iterator)。由它處理進(jìn)一步的迭代過程。
一個(gè)迭代器必須有 next() 方法,它返回一個(gè) {done: Boolean, value: any} 對(duì)象,這里 done:true 表明迭代結(jié)束,否則 value 就是下一個(gè)值。
Symbol.iterator 方法會(huì)被 for..of 自動(dòng)調(diào)用,但我們也可以直接調(diào)用它。
內(nèi)置的可迭代對(duì)象例如字符串和數(shù)組,都實(shí)現(xiàn)了 Symbol.iterator。
字符串迭代器能夠識(shí)別代理對(duì)(surrogate pair)。(譯注:代理對(duì)也就是 UTF-16 擴(kuò)展字符。)
有索引屬性和 length 屬性的對(duì)象被稱為 類數(shù)組對(duì)象。這種對(duì)象可能還具有其他屬性和方法,但是沒有數(shù)組的內(nèi)建方法。
如果我們仔細(xì)研究一下規(guī)范 —— 就會(huì)發(fā)現(xiàn)大多數(shù)內(nèi)建方法都假設(shè)它們需要處理的是可迭代對(duì)象或者類數(shù)組對(duì)象,而不是“真正的”數(shù)組,因?yàn)檫@樣抽象度更高。
Array.from(obj[, mapFn, thisArg]) 將可迭代對(duì)象或類數(shù)組對(duì)象 obj 轉(zhuǎn)化為真正的數(shù)組 Array,然后我們就可以對(duì)它應(yīng)用數(shù)組的方法。可選參數(shù) mapFn 和 thisArg 允許我們將函數(shù)應(yīng)用到每個(gè)元素。
21. Map and Set(映射和集合)
Map —— 是一個(gè)帶鍵的數(shù)據(jù)項(xiàng)的集合。
方法和屬性如下:
new Map([iterable]) —— 創(chuàng)建 map,可選擇帶有 [key,value] 對(duì)的 iterable(例如數(shù)組)來進(jìn)行初始化。
map.set(key, value) —— 根據(jù)鍵存儲(chǔ)值。
map.get(key) —— 根據(jù)鍵來返回值,如果 map 中不存在對(duì)應(yīng)的 key,則返回 undefined。
map.has(key) —— 如果 key 存在則返回 true,否則返回 false。
map.delete(key) —— 刪除指定鍵的值。
map.clear() —— 清空 map 。
map.size —— 返回當(dāng)前元素個(gè)數(shù)。
與普通對(duì)象 Object 的不同點(diǎn):
任何鍵、對(duì)象都可以作為鍵。
有其他的便捷方法,如 size 屬性。
Set —— 是一組唯一值的集合。
方法和屬性:
new Set([iterable]) —— 創(chuàng)建 set,可選擇帶有 iterable(例如數(shù)組)來進(jìn)行初始化。
set.add(value) —— 添加一個(gè)值(如果 value 存在則不做任何修改),返回 set 本身。
set.delete(value) —— 刪除值,如果 value 在這個(gè)方法調(diào)用的時(shí)候存在則返回 true ,否則返回 false。
set.has(value) —— 如果 value 在 set 中,返回 true,否則返回 false。
set.clear() —— 清空 set。
set.size —— 元素的個(gè)數(shù)。
在 Map 和 Set 中迭代總是按照值插入的順序進(jìn)行的,所以我們不能說這些集合是無序的,但是我們不能對(duì)元素進(jìn)行重新排序,也不能直接按其編號(hào)來獲取元素。
22. WeakMap and WeakSet(弱映射和弱集合)
WeakMap 是類似于 Map 的集合,它僅允許對(duì)象作為鍵,并且一旦通過其他方式無法訪問它們,便會(huì)將它們與其關(guān)聯(lián)值一同刪除。
WeakSet 是類似于 Set 的集合,它僅存儲(chǔ)對(duì)象,并且一旦通過其他方式無法訪問它們,便會(huì)將其刪除。
它們都不支持引用所有鍵或其計(jì)數(shù)的方法和屬性。僅允許單個(gè)操作。
WeakMap 和 WeakSet 被用作“主要”對(duì)象存儲(chǔ)之外的“輔助”數(shù)據(jù)結(jié)構(gòu)。一旦將對(duì)象從主存儲(chǔ)器中刪除,如果該對(duì)象僅被用作 WeakMap 或 WeakSet 的鍵,那么它將被自動(dòng)清除。
23. 解構(gòu)賦值
解構(gòu)賦值可以立即將一個(gè)對(duì)象或數(shù)組映射到多個(gè)變量上。
解構(gòu)對(duì)象的完整語法:
let {prop : varName = default, ...rest} = object
這表示屬性 prop 會(huì)被賦值給變量 varName,如果沒有這個(gè)屬性的話,就會(huì)使用默認(rèn)值 default。
沒有對(duì)應(yīng)映射的對(duì)象屬性會(huì)被復(fù)制到 rest 對(duì)象。
解構(gòu)數(shù)組的完整語法:
let [item1 = default, item2, ...rest] = array
數(shù)組的第一個(gè)元素被賦值給 item1,第二個(gè)元素被賦值給 item2,剩下的所有元素被復(fù)制到另一個(gè)數(shù)組 rest。
從嵌套數(shù)組/對(duì)象中提取數(shù)據(jù)也是可以的,此時(shí)等號(hào)左側(cè)必須和等號(hào)右側(cè)有相同的結(jié)構(gòu)。
24. 日期和時(shí)間
在 JavaScript 中,日期和時(shí)間使用 Date[16] 對(duì)象來表示。我們不能只創(chuàng)建日期,或者只創(chuàng)建時(shí)間,Date 對(duì)象總是同時(shí)創(chuàng)建兩者。
月份從 0 開始計(jì)數(shù)(對(duì),一月是 0)。
一周中的某一天 getDay() 同樣從 0 開始計(jì)算(0 代表星期日)。
當(dāng)設(shè)置了超出范圍的組件時(shí),Date 會(huì)進(jìn)行自我校準(zhǔn)。這一點(diǎn)對(duì)于日/月/小時(shí)的加減很有用。
日期可以相減,得到的是以毫秒表示的兩者的差值。因?yàn)楫?dāng) Date 被轉(zhuǎn)換為數(shù)字時(shí),Date 對(duì)象會(huì)被轉(zhuǎn)換為時(shí)間戳。
使用 Date.now() 可以更快地獲取當(dāng)前時(shí)間的時(shí)間戳。
和其他系統(tǒng)不同,JavaScript 中時(shí)間戳以毫秒為單位,而不是秒。
有時(shí)我們需要更加精準(zhǔn)的時(shí)間度量。JavaScript 自身并沒有測(cè)量微秒的方法(百萬分之一秒),但大多數(shù)運(yùn)行環(huán)境會(huì)提供。例如:瀏覽器有 performance.now\(\)[17] 方法來給出從頁面加載開始的以毫秒為單位的微秒數(shù)(精確到毫秒的小數(shù)點(diǎn)后三位):
alert(`Loading started ${performance.now()}ms ago`);
// 類似于 "Loading started 34731.26000000001ms ago"
// .26 表示的是微秒(260 微秒)
// 小數(shù)點(diǎn)后超過 3 位的數(shù)字是精度錯(cuò)誤,只有前三位數(shù)字是正確的
復(fù)制代碼
Node.js 有 microtime 模塊以及其他方法。從技術(shù)上講,幾乎所有的設(shè)備和環(huán)境都允許獲取更高精度的數(shù)值,只是不是通過 Date 對(duì)象。
25. JSON 方法,toJSON
JSON 是一種數(shù)據(jù)格式,具有自己的獨(dú)立標(biāo)準(zhǔn)和大多數(shù)編程語言的庫。
JSON 支持 object,array,string,number,boolean 和 null。
JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify[18] 和解析 JSON 的方法 JSON.parse[19]。
這兩種方法都支持用于智能讀/寫的轉(zhuǎn)換函數(shù)。
如果一個(gè)對(duì)象具有 toJSON,那么它會(huì)被 JSON.stringify 調(diào)用。
26. 遞歸和堆棧
術(shù)語:
遞歸 是編程的一個(gè)術(shù)語,表示從自身調(diào)用函數(shù)(譯注:也就是自調(diào)用)。遞歸函數(shù)可用于以更優(yōu)雅的方式解決問題。
當(dāng)一個(gè)函數(shù)調(diào)用自身時(shí),我們稱其為 遞歸步驟。遞歸的 基礎(chǔ) 是函數(shù)參數(shù)使任務(wù)簡單到該函數(shù)不再需要進(jìn)行進(jìn)一步調(diào)用。
遞歸定義[20] 的數(shù)據(jù)結(jié)構(gòu)是指可以使用自身來定義的數(shù)據(jù)結(jié)構(gòu)。
例如,鏈表可以被定義為由對(duì)象引用一個(gè)列表(或 null)而組成的數(shù)據(jù)結(jié)構(gòu)。
list = { value, next -> list }
復(fù)制代碼
像 HTML 元素樹或者本章中的 department 樹等,本質(zhì)上也是遞歸:它們有分支,而且分支又可以有其他分支。
就像我們?cè)谑纠?sumSalary 中看到的那樣,可以使用遞歸函數(shù)來遍歷它們。
任何遞歸函數(shù)都可以被重寫為迭代(譯注:也就是循環(huán))形式。有時(shí)這是在優(yōu)化代碼時(shí)需要做的。但對(duì)于大多數(shù)任務(wù)來說,遞歸方法足夠快,并且容易編寫和維護(hù)。
27. Rest 參數(shù)與 Spread 語法
當(dāng)我們?cè)诖a中看到 "..." 時(shí),它要么是 rest 參數(shù),要么就是 spread 語法。
有一個(gè)簡單的方法可以區(qū)分它們:
若 ... 出現(xiàn)在函數(shù)參數(shù)列表的最后,那么它就是 rest 參數(shù),它會(huì)把參數(shù)列表中剩余的參數(shù)收集到一個(gè)數(shù)組中。
若 ... 出現(xiàn)在函數(shù)調(diào)用或類似的表達(dá)式中,那它就是 spread 語法,它會(huì)把一個(gè)數(shù)組展開為列表。
使用場(chǎng)景:
Rest 參數(shù)用于創(chuàng)建可接受任意數(shù)量參數(shù)的函數(shù)。
Spread 語法用于將數(shù)組傳遞給通常需要含有許多參數(shù)的列表的函數(shù)。
它們倆的出現(xiàn)幫助我們輕松地在列表和參數(shù)數(shù)組之間來回轉(zhuǎn)換。
“舊式”的 arguments(類數(shù)組且可迭代的對(duì)象)也依然能夠幫助我們獲取函數(shù)調(diào)用中的所有參數(shù)。
28. 全局對(duì)象
全局對(duì)象包含應(yīng)該在任何位置都可見的變量。
其中包括 JavaScript 的內(nèi)建方法,例如 “Array” 和環(huán)境特定(environment-specific)的值,例如 window.innerHeight — 瀏覽器中的窗口高度。
全局對(duì)象有一個(gè)通用名稱 globalThis。
……但是更常見的是使用“老式”的環(huán)境特定(environment-specific)的名字,例如 window(瀏覽器)和 global(Node.js)。
僅當(dāng)值對(duì)于我們的項(xiàng)目而言確實(shí)是全局的時(shí),才應(yīng)將其存儲(chǔ)在全局對(duì)象中。并保持其數(shù)量最少。
在瀏覽器中,除非我們使用 modules[21],否則使用 var 聲明的全局函數(shù)和變量會(huì)成為全局對(duì)象的屬性。
為了使我們的代碼面向未來并更易于理解,我們應(yīng)該使用直接的方式訪問全局對(duì)象的屬性,如 window.x。
29. 函數(shù)對(duì)象,NFE
函數(shù)就是對(duì)象。
我們介紹了它們的一些屬性:
name —— 函數(shù)的名字。通常取自函數(shù)定義,但如果函數(shù)定義時(shí)沒設(shè)定函數(shù)名,JavaScript 會(huì)嘗試通過函數(shù)的上下文猜一個(gè)函數(shù)名(例如把賦值的變量名取為函數(shù)名)。
length —— 函數(shù)定義時(shí)的入?yún)⒌膫€(gè)數(shù)。Rest 參數(shù)不參與計(jì)數(shù)。
如果函數(shù)是通過函數(shù)表達(dá)式的形式被聲明的(不是在主代碼流里),并且附帶了名字,那么它被稱為命名函數(shù)表達(dá)式(Named Function Expression)。這個(gè)名字可以用于在該函數(shù)內(nèi)部進(jìn)行自調(diào)用,例如遞歸調(diào)用等。
此外,函數(shù)可以帶有額外的屬性。很多知名的 JavaScript 庫都充分利用了這個(gè)功能。
它們創(chuàng)建一個(gè)“主”函數(shù),然后給它附加很多其它“輔助”函數(shù)。例如,jQuery[22] 庫創(chuàng)建了一個(gè)名為 $ 的函數(shù)。lodash[23] 庫創(chuàng)建一個(gè) _ 函數(shù),然后為其添加了 _.add、_.keyBy 以及其它屬性(想要了解更多內(nèi)容,參查閱 docs[24])。實(shí)際上,它們這么做是為了減少對(duì)全局空間的污染,這樣一個(gè)庫就只會(huì)有一個(gè)全局變量。這樣就降低了命名沖突的可能性。
所以,一個(gè)函數(shù)本身可以完成一項(xiàng)有用的工作,還可以在自身的屬性中附帶許多其他功能。
29. "new Function" 語法
語法:
let func = new Function ([arg1, arg2, ...argN], functionBody);
復(fù)制代碼
由于歷史原因,參數(shù)也可以按逗號(hào)分隔符的形式給出。
以下三種聲明的含義相同:
new Function('a', 'b', 'return a + b'); // 基礎(chǔ)語法
new Function('a,b', 'return a + b'); // 逗號(hào)分隔
new Function('a , b', 'return a + b'); // 逗號(hào)和空格分隔
復(fù)制代碼
使用 new Function 創(chuàng)建的函數(shù),它的 [[Environment]] 指向全局詞法環(huán)境,而不是函數(shù)所在的外部詞法環(huán)境。因此,我們不能在 new Function 中直接使用外部變量。不過這樣是好事,這有助于降低我們代碼出錯(cuò)的可能。并且,從代碼架構(gòu)上講,顯式地使用參數(shù)傳值是一種更好的方法,并且避免了與使用壓縮程序而產(chǎn)生沖突的問題。
30. 調(diào)度:setTimeout 和 setInterval
setTimeout(func, delay, ...args) 和 setInterval(func, delay, ...args) 方法允許我們?cè)?delay 毫秒之后運(yùn)行 func 一次或以 delay 毫秒為時(shí)間間隔周期性運(yùn)行 func。
要取消函數(shù)的執(zhí)行,我們應(yīng)該調(diào)用 clearInterval/clearTimeout,并將 setInterval/setTimeout 返回的值作為入?yún)魅搿?/p>
嵌套的 setTimeout 比 setInterval 用起來更加靈活,允許我們更精確地設(shè)置兩次執(zhí)行之間的時(shí)間。
零延時(shí)調(diào)度 setTimeout(func, 0)(與 setTimeout(func) 相同)用來調(diào)度需要盡快執(zhí)行的調(diào)用,但是會(huì)在當(dāng)前腳本執(zhí)行完成后進(jìn)行調(diào)用。
瀏覽器會(huì)將 setTimeout 或 setInterval 的五層或更多層嵌套調(diào)用(調(diào)用五次之后)的最小延時(shí)限制在 4ms。這是歷史遺留問題。
請(qǐng)注意,所有的調(diào)度方法都不能 保證 確切的延時(shí)。
例如,瀏覽器內(nèi)的計(jì)時(shí)器可能由于許多原因而變慢:
CPU 過載。
瀏覽器頁簽處于后臺(tái)模式。
筆記本電腦用的是電池供電(譯注:使用電池供電會(huì)以降低性能為代價(jià)提升續(xù)航)。
所有這些因素,可能會(huì)將定時(shí)器的最小計(jì)時(shí)器分辨率(最小延遲)增加到 300ms 甚至 1000ms,具體以瀏覽器及其設(shè)置為準(zhǔn)。
31. 裝飾器模式和轉(zhuǎn)發(fā),call/apply
裝飾器 是一個(gè)圍繞改變函數(shù)行為的包裝器。主要工作仍由該函數(shù)來完成。
裝飾器可以被看作是可以添加到函數(shù)的 “features” 或 “aspects”。我們可以添加一個(gè)或添加多個(gè)。而這一切都無需更改其代碼!
為了實(shí)現(xiàn) cachingDecorator,我們研究了以下方法:
func.call\(context, arg1, arg2…\)[25] —— 用給定的上下文和參數(shù)調(diào)用 func。
func.apply\(context, args\)[26] —— 調(diào)用 func 將 context 作為 this 和類數(shù)組的 args 傳遞給參數(shù)列表。
通用的 呼叫轉(zhuǎn)移(call forwarding) 通常是使用 apply 完成的:
let wrapper = function() {
return original.apply(this, arguments);
};
復(fù)制代碼
我們也可以看到一個(gè) 方法借用(method borrowing) 的例子,就是我們從一個(gè)對(duì)象中獲取一個(gè)方法,并在另一個(gè)對(duì)象的上下文中“調(diào)用”它。采用數(shù)組方法并將它們應(yīng)用于參數(shù) arguments 是很常見的。另一種方法是使用 Rest 參數(shù)對(duì)象,該對(duì)象是一個(gè)真正的數(shù)組。
32. 函數(shù)綁定
方法 func.bind(context, ...args) 返回函數(shù) func 的“綁定的(bound)變體”,它綁定了上下文 this 和第一個(gè)參數(shù)(如果給定了)。
通常我們應(yīng)用 bind 來綁定對(duì)象方法的 this,這樣我們就可以把它們傳遞到其他地方使用。例如,傳遞給 setTimeout。
當(dāng)我們綁定一個(gè)現(xiàn)有的函數(shù)的某些參數(shù)時(shí),綁定后的(不太通用的)函數(shù)被稱為 partially applied 或 partial。
當(dāng)我們不想一遍又一遍地重復(fù)相同的參數(shù)時(shí),partial 非常有用。就像我們有一個(gè) send(from, to) 函數(shù),并且對(duì)于我們的任務(wù)來說,from 應(yīng)該總是一樣的,那么我們就可以搞一個(gè) partial 并使用它。
33. 深入理解箭頭函數(shù)
箭頭函數(shù):
沒有 this
沒有 arguments
不能使用 new 進(jìn)行調(diào)用
它們也沒有 super,但目前我們還沒有學(xué)到它。我們將在 類繼承[27] 一章中學(xué)習(xí)它。
這是因?yàn)椋^函數(shù)是針對(duì)那些沒有自己的“上下文”,但在當(dāng)前上下文中起作用的短代碼的。并且箭頭函數(shù)確實(shí)在這種使用場(chǎng)景中大放異彩。
34. 原型繼承
在 JavaScript 中,所有的對(duì)象都有一個(gè)隱藏的 [[Prototype]] 屬性,它要么是另一個(gè)對(duì)象,要么就是 null。
我們可以使用 obj.__proto__ 訪問它(歷史遺留下來的 getter/setter,這兒還有其他方法,很快我們就會(huì)講到)。
通過 [[Prototype]] 引用的對(duì)象被稱為“原型”。
如果我們想要讀取 obj 的一個(gè)屬性或者調(diào)用一個(gè)方法,并且它不存在,那么 JavaScript 就會(huì)嘗試在原型中查找它。
寫/刪除操作直接在對(duì)象上進(jìn)行,它們不使用原型(假設(shè)它是數(shù)據(jù)屬性,不是 setter)。
如果我們調(diào)用 obj.method(),而且 method 是從原型中獲取的,this 仍然會(huì)引用 obj。因此,方法始終與當(dāng)前對(duì)象一起使用,即使方法是繼承的。
for..in 循環(huán)在其自身和繼承的屬性上進(jìn)行迭代。所有其他的鍵/值獲取方法僅對(duì)對(duì)象本身起作用。
35. F.prototype
一切都很簡單,只需要記住幾條重點(diǎn)就可以清晰地掌握了:
F.prototype 屬性(不要把它與 [[Prototype]] 弄混了)在 new F 被調(diào)用時(shí)為新對(duì)象的 [[Prototype]] 賦值。
F.prototype 的值要么是一個(gè)對(duì)象,要么就是 null:其他值都不起作用。
"prototype" 屬性僅在設(shè)置了一個(gè)構(gòu)造函數(shù)(constructor function),并通過 new 調(diào)用時(shí),才具有這種特殊的影響。
在常規(guī)對(duì)象上,prototype 沒什么特別的:
let user = {
name: "John",
prototype: "Bla-bla" // 這里只是普通的屬性
};
復(fù)制代碼
默認(rèn)情況下,所有函數(shù)都有 F.prototype = {constructor:F},所以我們可以通過訪問它的 "constructor" 屬性來獲取一個(gè)對(duì)象的構(gòu)造器。
36. 原生的原型
所有的內(nèi)建對(duì)象都遵循相同的模式(pattern):
方法都存儲(chǔ)在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)。
對(duì)象本身只存儲(chǔ)數(shù)據(jù)(數(shù)組元素、對(duì)象屬性、日期)。
原始數(shù)據(jù)類型也將方法存儲(chǔ)在包裝器對(duì)象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype。只有 undefined 和 null 沒有包裝器對(duì)象。
內(nèi)建原型可以被修改或被用新的方法填充。但是不建議更改它們。唯一允許的情況可能是,當(dāng)我們添加一個(gè)還沒有被 JavaScript 引擎支持,但已經(jīng)被加入 JavaScript 規(guī)范的新標(biāo)準(zhǔn)時(shí),才可能允許這樣做。
37. 原型方法,沒有 __proto__ 的對(duì)象
設(shè)置和直接訪問原型的現(xiàn)代方法有:
Object.create\(proto, \[descriptors\]\)[28] —— 利用給定的 proto 作為 [[Prototype]](可以是 null)和可選的屬性描述來創(chuàng)建一個(gè)空對(duì)象。
Object.getPrototypeOf\(obj\)[29] —— 返回對(duì)象 obj 的 [[Prototype]](與 __proto__ 的 getter 相同)。
Object.setPrototypeOf\(obj, proto\)[30] —— 將對(duì)象 obj 的 [[Prototype]] 設(shè)置為 proto(與 __proto__ 的 setter 相同)。
如果要將一個(gè)用戶生成的鍵放入一個(gè)對(duì)象,那么內(nèi)建的 __proto__ getter/setter 是不安全的。因?yàn)橛脩艨赡軙?huì)輸入 "__proto__" 作為鍵,這會(huì)導(dǎo)致一個(gè) error,雖然我們希望這個(gè)問題不會(huì)造成什么大影響,但通常會(huì)造成不可預(yù)料的后果。
因此,我們可以使用 Object.create(null) 創(chuàng)建一個(gè)沒有 __proto__ 的 “very plain” 對(duì)象,或者對(duì)此類場(chǎng)景堅(jiān)持使用 Map 對(duì)象就可以了。
此外,Object.create 提供了一種簡單的方式來淺拷貝一個(gè)對(duì)象的所有描述符:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
復(fù)制代碼
此外,我們還明確了 __proto__ 是 [[Prototype]] 的 getter/setter,就像其他方法一樣,它位于 Object.prototype。
我們可以通過 Object.create(null) 來創(chuàng)建沒有原型的對(duì)象。這樣的對(duì)象被用作 “pure dictionaries”,對(duì)于它們而言,使用 "__proto__" 作為鍵是沒有問題的。
其他方法:
Object.keys\(obj\)[31] / Object.values\(obj\)[32] / Object.entries\(obj\)[33] —— 返回一個(gè)可枚舉的由自身的字符串屬性名/值/鍵值對(duì)組成的數(shù)組。
Object.getOwnPropertySymbols\(obj\)[34] —— 返回一個(gè)由自身所有的 symbol 類型的鍵組成的數(shù)組。
Object.getOwnPropertyNames\(obj\)[35] —— 返回一個(gè)由自身所有的字符串鍵組成的數(shù)組。
Reflect.ownKeys\(obj\)[36] —— 返回一個(gè)由自身所有鍵組成的數(shù)組。
obj.hasOwnProperty\(key\)[37]:如果 obj 擁有名為 key 的自身的屬性(非繼承而來的),則返回 true。
所有返回對(duì)象屬性的方法(如 Object.keys 及其他)—— 都返回“自身”的屬性。如果我們想繼承它們,我們可以使用 for...in。
38. Class 基本語法
基本的類語法看起來像這樣:
class MyClass {
prop = value; // 屬性
constructor(...) { // 構(gòu)造器
// ...
}
method(...) {} // method
get something(...) {} // getter 方法
set something(...) {} // setter 方法
[Symbol.iterator]() {} // 有計(jì)算名稱(computed name)的方法(此處為 symbol)
// ...
}
復(fù)制代碼
技術(shù)上來說,MyClass 是一個(gè)函數(shù)(我們提供作為 constructor 的那個(gè)),而 methods、getters 和 settors 都被寫入了 MyClass.prototype。
39. 類繼承
想要擴(kuò)展一個(gè)類:class Child extends Parent:
這意味著 Child.prototype.__proto__ 將是 Parent.prototype,所以方法會(huì)被繼承。
重寫一個(gè) constructor:
在使用 this 之前,我們必須在 Child 的 constructor 中將父 constructor 調(diào)用為 super()。
重寫一個(gè)方法:
我們可以在一個(gè) Child 方法中使用 super.method() 來調(diào)用 Parent 方法。
內(nèi)部:
方法在內(nèi)部的 [[HomeObject]] 屬性中記住了它們的類/對(duì)象。這就是 super 如何解析父方法的。
因此,將一個(gè)帶有 super 的方法從一個(gè)對(duì)象復(fù)制到另一個(gè)對(duì)象是不安全的。
補(bǔ)充:
箭頭函數(shù)沒有自己的 this 或 super,所以它們能融入到就近的上下文中,像透明似的。
40. 靜態(tài)屬性和靜態(tài)方法
靜態(tài)方法被用于實(shí)現(xiàn)屬于整個(gè)類的功能。它與具體的類實(shí)例無關(guān)。
舉個(gè)例子, 一個(gè)用于進(jìn)行比較的方法 Article.compare(article1, article2) 或一個(gè)工廠(factory)方法 Article.createTodays()。
在類生命中,它們都被用關(guān)鍵字 static 進(jìn)行了標(biāo)記。
靜態(tài)屬性被用于當(dāng)我們想要存儲(chǔ)類級(jí)別的數(shù)據(jù)時(shí),而不是綁定到實(shí)例。
語法如下所示:
class MyClass {
static property = ...;
static method() {
...
}
}
復(fù)制代碼
從技術(shù)上講,靜態(tài)聲明與直接給類本身賦值相同:
MyClass.property = ...
MyClass.method = ...
復(fù)制代碼
靜態(tài)屬性和方法是可被繼承的。
對(duì)于 class B extends A,類 B 的 prototype 指向了 A:B.[[Prototype]] = A。因此,如果一個(gè)字段在 B 中沒有找到,會(huì)繼續(xù)在 A 中查找。
41. 私有的和受保護(hù)的屬性和方法
就面向?qū)ο缶幊蹋∣OP)而言,內(nèi)部接口與外部接口的劃分被稱為 封裝[38]。
它具有以下優(yōu)點(diǎn):
保護(hù)用戶,使他們不會(huì)誤傷自己
想象一下,有一群開發(fā)人員在使用一個(gè)咖啡機(jī)。這個(gè)咖啡機(jī)是由“最好的咖啡機(jī)”公司制造的,工作正常,但是保護(hù)罩被拿掉了。因此內(nèi)部接口暴露了出來。
所有的開發(fā)人員都是文明的 —— 他們按照預(yù)期使用咖啡機(jī)。但其中的一個(gè)人,約翰,他認(rèn)為自己是最聰明的人,并對(duì)咖啡機(jī)的內(nèi)部做了一些調(diào)整。然而,咖啡機(jī)兩天后就壞了。
這肯定不是約翰的錯(cuò),而是那個(gè)取下保護(hù)罩并讓約翰進(jìn)行操作的人的錯(cuò)。
編程也一樣。如果一個(gè) class 的使用者想要改變那些本不打算被從外部更改的東西 —— 后果是不可預(yù)測(cè)的。
可支持性
編程的情況比現(xiàn)實(shí)生活中的咖啡機(jī)要復(fù)雜得多,因?yàn)槲覀儾恢皇琴徺I一次。我們還需要不斷開發(fā)和改進(jìn)代碼。
如果我們嚴(yán)格界定內(nèi)部接口,那么這個(gè) class 的開發(fā)人員可以自由地更改其內(nèi)部屬性和方法,甚至無需通知用戶。
如果你是這樣的 class 的開發(fā)者,那么你會(huì)很高興知道可以安全地重命名私有變量,可以更改甚至刪除其參數(shù),因?yàn)闆]有外部代碼依賴于它們。
對(duì)于用戶來說,當(dāng)新版本問世時(shí),應(yīng)用的內(nèi)部可能被進(jìn)行了全面檢修,但如果外部接口相同,則仍然很容易升級(jí)。
隱藏復(fù)雜性
人們喜歡使用簡單的東西。至少從外部來看是這樣。內(nèi)部的東西則是另外一回事了。
程序員也不例外。
當(dāng)實(shí)施細(xì)節(jié)被隱藏,并提供了簡單且有據(jù)可查的外部接口時(shí),總是很方便的。
為了隱藏內(nèi)部接口,我們使用受保護(hù)的或私有的屬性:
受保護(hù)的字段以 _ 開頭。這是一個(gè)眾所周知的約定,不是在語言級(jí)別強(qiáng)制執(zhí)行的。程序員應(yīng)該只通過它的類和從它繼承的類中訪問以 _ 開頭的字段。
私有字段以 # 開頭。JavaScript 確保我們只能從類的內(nèi)部訪問它們。
目前,各個(gè)瀏覽器對(duì)私有字段的支持不是很好,但可以用 polyfill 解決。
42. 類檢查:"instanceof"
讓我們總結(jié)一下我們知道的類型檢查方法:

當(dāng)我們使用類的層次結(jié)構(gòu)(hierarchy),并想要對(duì)該類進(jìn)行檢查,同時(shí)還要考慮繼承時(shí),這種場(chǎng)景下 instanceof 操作符確實(shí)很出色。正如我們所看到的,從技術(shù)上講,{}.toString 是一種“更高級(jí)的” typeof。
43. Mixin 模式
Mixin — 是一個(gè)通用的面向?qū)ο缶幊绦g(shù)語:一個(gè)包含其他類的方法的類。
一些其它編程語言允許多重繼承。JavaScript 不支持多重繼承,但是可以通過將方法拷貝到原型中來實(shí)現(xiàn) mixin。
我們可以使用 mixin 作為一種通過添加多種行為(例如上文中所提到的事件處理)來擴(kuò)充類的方法。
如果 Mixins 意外覆蓋了現(xiàn)有類的方法,那么它們可能會(huì)成為一個(gè)沖突點(diǎn)。因此,通常應(yīng)該仔細(xì)考慮 mixin 的命名方法,以最大程度地降低發(fā)生這種沖突的可能性。
44. 錯(cuò)誤處理,"try..catch"
try..catch 結(jié)構(gòu)允許我們處理執(zhí)行過程中出現(xiàn)的 error。從字面上看,它允許“嘗試”運(yùn)行代碼并“捕獲”其中可能發(fā)生的錯(cuò)誤。
語法如下:
try {
// 執(zhí)行此處代碼
} catch(err) {
// 如果發(fā)生錯(cuò)誤,跳轉(zhuǎn)至此處
// err 是一個(gè) error 對(duì)象
} finally {
// 無論怎樣都會(huì)在 try/catch 之后執(zhí)行
}
復(fù)制代碼
這兒可能會(huì)沒有 catch 部分或者沒有 finally,所以 try..catch 或 try..finally 都是可用的。
Error 對(duì)象包含下列屬性:
message — 人類可讀的 error 信息。
name — 具有 error 名稱的字符串(Error 構(gòu)造器的名稱)。
stack(沒有標(biāo)準(zhǔn),但得到了很好的支持)— Error 發(fā)生時(shí)的調(diào)用棧。
如果我們不需要 error 對(duì)象,我們可以通過使用 catch { 而不是 catch(err) { 來省略它。
我們也可以使用 throw 操作符來生成自定義的 error。從技術(shù)上講,throw 的參數(shù)可以是任何東西,但通常是繼承自內(nèi)建的 Error 類的 error 對(duì)象。下一章我們會(huì)詳細(xì)介紹擴(kuò)展 error。
再次拋出(rethrowing)是一種錯(cuò)誤處理的重要模式:catch 塊通常期望并知道如何處理特定的 error 類型,因此它應(yīng)該再次拋出它不知道的 error。
即使我們沒有 try..catch,大多數(shù)執(zhí)行環(huán)境也允許我們?cè)O(shè)置“全局”錯(cuò)誤處理程序來捕獲“掉出(fall out)”的 error。在瀏覽器中,就是 window.onerror。
45. 自定義 Error,擴(kuò)展 Error
我們可以正常地從 Error 和其他內(nèi)建的 error 類中進(jìn)行繼承,。我們只需要注意 name 屬性以及不要忘了調(diào)用 super。
我們可以使用 instanceof 來檢查特定的 error。但有時(shí)我們有來自第三方庫的 error 對(duì)象,并且在這兒沒有簡單的方法來獲取它的類。那么可以將 name 屬性用于這一類的檢查。
包裝異常是一項(xiàng)廣泛應(yīng)用的技術(shù):用于處理低級(jí)別異常并創(chuàng)建高級(jí)別 error 而不是各種低級(jí)別 error 的函數(shù)。在上面的示例中,低級(jí)別異常有時(shí)會(huì)成為該對(duì)象的屬性,例如 err.cause,但這不是嚴(yán)格要求的。
46. Promise 鏈
如果 .then(或 catch/finally 都可以)處理程序(handler)返回一個(gè) promise,那么鏈的其余部分將會(huì)等待,直到它狀態(tài)變?yōu)?settled。當(dāng)它被 settled 后,其 result(或 error)將被進(jìn)一步傳遞下去。
這是一個(gè)完整的流程圖:

47. 使用 promise 進(jìn)行錯(cuò)誤處理
.catch 處理 promise 中的各種 error:在 reject() 調(diào)用中的,或者在處理程序(handler)中拋出的(thrown)error。
我們應(yīng)該將 .catch 準(zhǔn)確地放到我們想要處理 error,并知道如何處理這些 error 的地方。處理程序應(yīng)該分析 error(可以自定義 error 類來幫助分析)并再次拋出未知的 error(可能它們是編程錯(cuò)誤)。
如果沒有辦法從 error 中恢復(fù)的話,不使用 .catch 也可以。
在任何情況下我們都應(yīng)該有 unhandledrejection 事件處理程序(用于瀏覽器,以及其他環(huán)境的模擬),以跟蹤未處理的 error 并告知用戶(可能還有我們的服務(wù)器)有關(guān)信息,以使我們的應(yīng)用程序永遠(yuǎn)不會(huì)“死掉”。
補(bǔ)充內(nèi)容
Fetch 錯(cuò)誤處理示例
讓我們改進(jìn)用戶加載(user-loading)示例的錯(cuò)誤處理。
當(dāng)請(qǐng)求無法發(fā)出時(shí),fetch[39] reject 會(huì)返回 promise。例如,遠(yuǎn)程服務(wù)器無法訪問,或者 URL 異常。但是如果遠(yuǎn)程服務(wù)器返回響應(yīng)錯(cuò)誤 404,甚至是錯(cuò)誤 500,這些都被認(rèn)為是合法的響應(yīng)。
如果在 (*) 行,服務(wù)器返回一個(gè)錯(cuò)誤 500 的非 JSON(non-JSON)頁面該怎么辦?如果沒有這個(gè)用戶,GitHub 返回錯(cuò)誤 404 的頁面又該怎么辦呢?
fetch('no-such-user.json') // (*)
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`)) // (**)
.then(response => response.json())
.catch(alert); // SyntaxError: Unexpected token < in JSON at position 0
// ...
復(fù)制代碼
到目前為止,代碼試圖以 JSON 格式加載響應(yīng)數(shù)據(jù),但無論如何都會(huì)因?yàn)檎Z法錯(cuò)誤而失敗。你可以通過執(zhí)行上述例子來查看相關(guān)信息,因?yàn)槲募?no-such-user.json 不存在。
這有點(diǎn)糟糕,因?yàn)殄e(cuò)誤只是落在鏈上,并沒有相關(guān)細(xì)節(jié)信息:什么失敗了,在哪里失敗的。
因此我們多添加一步:我們應(yīng)該檢查具有 HTTP 狀態(tài)的 response.status 屬性,如果不是 200 就拋出錯(cuò)誤。
class HttpError extends Error { // (1)
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
function loadJson(url) { // (2)
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
})
}
loadJson('no-such-user.json') // (3)
.catch(alert); // HttpError: 404 for .../no-such-user.json
復(fù)制代碼
我們?yōu)?HTTP 錯(cuò)誤創(chuàng)建一個(gè)自定義類用于區(qū)分 HTTP 錯(cuò)誤和其他類型錯(cuò)誤。此外,新的類有一個(gè) constructor,它接受 response 對(duì)象,并將其保存到 error 中。因此,錯(cuò)誤處理(error-handling)代碼就能夠獲得響應(yīng)數(shù)據(jù)了。
然后我們將請(qǐng)求(requesting)和錯(cuò)誤處理代碼包裝進(jìn)一個(gè)函數(shù),它能夠 fetch url 并 將所有狀態(tài)碼不是 200 視為錯(cuò)誤。這很方便,因?yàn)槲覀兺ǔP枰@樣的邏輯。
現(xiàn)在 alert 顯示更多有用的描述信息。
擁有我們自己的錯(cuò)誤處理類的好處是我們可以使用 instanceof 很容易地在錯(cuò)誤處理代碼中檢查錯(cuò)誤。
例如,我們可以創(chuàng)建請(qǐng)求,如果我們得到 404 就可以告知用戶修改信息。
下面的代碼從 GitHub 加載給定名稱的用戶。如果沒有這個(gè)用戶,它將告知用戶填寫正確的名稱:
function demoGithubUser() {
let name = prompt("Enter a name?", "iliakan");
return loadJson(`https://api.github.com/users/${name}`)
.then(user => {
alert(`Full name: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err; // (*)
}
});
}
demoGithubUser();
復(fù)制代碼
請(qǐng)注意:這里的 .catch 會(huì)捕獲所有錯(cuò)誤,但是它僅僅“知道如何處理” HttpError 404。在那種特殊情況下,它意味著沒有這樣的用戶,而 .catch 僅僅在這種情況下重試。
對(duì)于其他錯(cuò)誤,它不知道會(huì)出現(xiàn)什么問題。可能是編程錯(cuò)誤或者其他錯(cuò)誤。所以它僅僅是在 (*) 行再次拋出。
其他
如果我們有加載指示(load-indication),.finally 是一個(gè)很好的處理程序(handler),在 fetch 完成時(shí)停止它:
function demoGithubUser() {
let name = prompt("Enter a name?", "iliakan");
document.body.style.opacity = 0.3; // (1) 開始指示(indication)
return loadJson(`https://api.github.com/users/${name}`)
.finally(() => { // (2) 停止指示(indication)
document.body.style.opacity = '';
return new Promise(resolve => setTimeout(resolve)); // (*)
})
.then(user => {
alert(`Full name: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
復(fù)制代碼
此處的 (1) 行,我們通過調(diào)暗文檔來指示加載。指示方法沒有什么問題,可以使用任何類型的指示來代替。
當(dāng) promise 得以解決,fetch 可以是成功或者錯(cuò)誤,finally 在 (2) 行觸發(fā)并終止加載指示。
有一個(gè)瀏覽器技巧,(*) 是從 finally 返回零延時(shí)(zero-timeout)的 promise。這是因?yàn)橐恍g覽器(比如 Chrome)需要“一點(diǎn)時(shí)間”外的 promise 處理程序來繪制文檔的更改。因此它確保在進(jìn)入鏈下一步之前,指示在視覺上是停止的。
48. Promise API
Promise 類有 5 種靜態(tài)方法:
Promise.all(promises) —— 等待所有 promise 都 resolve 時(shí),返回存放它們結(jié)果的數(shù)組。如果給定的任意一個(gè) promise 為 reject,那么它就會(huì)變成 Promise.all 的 error,所有其他 promise 的結(jié)果都會(huì)被忽略。
Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 時(shí),并以包含以下內(nèi)容的對(duì)象數(shù)組的形式返回它們的結(jié)果:
status: "fulfilled" 或 "rejected"
value(如果 fulfilled)或 reason(如果 rejected)。
Promise.race(promises) —— 等待第一個(gè) settle 的 promise,并將其 result/error 作為結(jié)果。
Promise.resolve(value) —— 使用給定 value 創(chuàng)建一個(gè) resolved 的 promise。
Promise.reject(error) —— 使用給定 error 創(chuàng)建一個(gè) rejected 的 promise。
這五個(gè)方法中,Promise.all 可能是在實(shí)戰(zhàn)中使用最多的。
49. 微任務(wù)(Microtask)
Promise 處理始終是異步的,因?yàn)樗?promise 行為都會(huì)通過內(nèi)部的 “promise jobs” 隊(duì)列,也被稱為“微任務(wù)隊(duì)列”(ES8 術(shù)語)。
因此,.then/catch/finally 處理程序(handler)總是在當(dāng)前代碼完成后才會(huì)被調(diào)用。
如果我們需要確保一段代碼在 .then/catch/finally 之后被執(zhí)行,我們可以將它添加到鏈?zhǔn)秸{(diào)用的 .then 中。
在大多數(shù) JavaScript 引擎中(包括瀏覽器和 Node.js),微任務(wù)(microtask)的概念與“事件循環(huán)(event loop)”和“宏任務(wù)(macrotasks)”緊密相關(guān)。
50. Async/await
函數(shù)前面的關(guān)鍵字 async 有兩個(gè)作用:
讓這個(gè)函數(shù)總是返回一個(gè) promise。
允許在該函數(shù)內(nèi)使用 await。
Promise 前的關(guān)鍵字 await 使 JavaScript 引擎等待該 promise settle,然后:
如果有 error,就會(huì)拋出異常 — 就像那里調(diào)用了 throw error 一樣。
否則,就返回結(jié)果。
這兩個(gè)關(guān)鍵字一起提供了一個(gè)很好的用來編寫異步代碼的框架,這種代碼易于閱讀也易于編寫。
有了 async/await 之后,我們就幾乎不需要使用 promise.then/catch,但是不要忘了它們是基于 promise 的,因?yàn)橛行r(shí)候(例如在最外層作用域)我們不得不使用這些方法。并且,當(dāng)我們需要同時(shí)等待需要任務(wù)時(shí),Promise.all 是很好用的。
51. Generator
Generator 是通過 generator 函數(shù) function* f(…) {…} 創(chuàng)建的。
在 generator(僅在)內(nèi)部,存在 yield 操作。
外部代碼和 generator 可能會(huì)通過 next/yield 調(diào)用交換結(jié)果。
在現(xiàn)代 JavaScript 中,generator 很少被使用。但有時(shí)它們會(huì)派上用場(chǎng),因?yàn)楹瘮?shù)在執(zhí)行過程中與調(diào)用代碼交換數(shù)據(jù)的能力是非常獨(dú)特的。而且,當(dāng)然,它們非常適合創(chuàng)建可迭代對(duì)象。
并且,在下一章我們將會(huì)學(xué)習(xí) async generator,它們被用于在 for await ... of 循環(huán)中讀取異步生成的數(shù)據(jù)流(例如,通過網(wǎng)絡(luò)分頁提取 (paginated fetches over a network))。
在 Web 編程中,我們經(jīng)常使用數(shù)據(jù)流,因此這是另一個(gè)非常重要的使用場(chǎng)景。
52. 異步迭代和 generator
常規(guī)的 iterator 和 generator 可以很好地處理那些不需要花費(fèi)時(shí)間來生成的的數(shù)據(jù)。
當(dāng)我們期望異步地,有延遲地獲取數(shù)據(jù)時(shí),可以使用它們的異步版本,并且使用 for await..of 替代 for..of。
異步 iterator 與常規(guī) iterator 在語法上的區(qū)別:

異步 generator 與常規(guī) generator 在語法上的區(qū)別:

在 Web 開發(fā)中,我們經(jīng)常會(huì)遇到數(shù)據(jù)流,它們分段流動(dòng)(flows chunk-by-chunk)。例如,下載或上傳大文件。
我們可以使用異步 generator 來處理此類數(shù)據(jù)。值得注意的是,在一些環(huán)境,例如瀏覽器環(huán)境下,還有另一個(gè)被稱為 Streams 的 API,它提供了特殊的接口來處理此類數(shù)據(jù)流,轉(zhuǎn)換數(shù)據(jù)并將數(shù)據(jù)從一個(gè)數(shù)據(jù)流傳遞到另一個(gè)數(shù)據(jù)流(例如,從一個(gè)地方下載并立即發(fā)送到其他地方)。
53. 模塊 (Module) 簡介
下面總結(jié)一下模塊的核心概念:
一個(gè)模塊就是一個(gè)文件。瀏覽器需要使用
默認(rèn)是延遲解析的(deferred)。
Async 可用于內(nèi)聯(lián)腳本。
要從另一個(gè)源(域/協(xié)議/端口)加載外部腳本,需要 CORS header。
重復(fù)的外部腳本會(huì)被忽略
模塊具有自己的本地頂級(jí)作用域,并可以通過 import/export 交換功能。
模塊始終使用 use strict。
模塊代碼只執(zhí)行一次。導(dǎo)出僅創(chuàng)建一次,然后會(huì)在導(dǎo)入之間共享。
當(dāng)我們使用模塊時(shí),每個(gè)模塊都會(huì)實(shí)現(xiàn)特定功能并將其導(dǎo)出。然后我們使用 import 將其直接導(dǎo)入到需要的地方即可。瀏覽器會(huì)自動(dòng)加載并解析腳本。
在生產(chǎn)環(huán)境中,出于性能和其他原因,開發(fā)者經(jīng)常使用諸如 Webpack[40] 之類的打包工具將模塊打包到一起。
54. 導(dǎo)出和導(dǎo)入
在聲明一個(gè) class/function/… 之前:
export [default] class/function/variable ... 獨(dú)立的導(dǎo)出:
export {x [as y], ...}. 重新導(dǎo)出:
export {x [as y], ...} from "module"
export * from "module"(不會(huì)重新導(dǎo)出默認(rèn)的導(dǎo)出)。
export {default [as y]} from "module"(重新導(dǎo)出默認(rèn)的導(dǎo)出)。
導(dǎo)入:
模塊中命名的導(dǎo)出:
import {x [as y], ...} from "module" 默認(rèn)的導(dǎo)出:
import x from "module"
import {default as x} from "module"
所有:
import * as obj from "module" 導(dǎo)入模塊(它的代碼,并運(yùn)行),但不要將其賦值給變量:
import "module"
我們把 import/export 語句放在腳本的頂部或底部,都沒關(guān)系。
因此,從技術(shù)上講,下面這樣的代碼沒有問題:
sayHi();
// ...
import {sayHi} from './say.js'; // 在文件底部導(dǎo)入
復(fù)制代碼
在實(shí)際開發(fā)中,導(dǎo)入通常位于文件的開頭,但是這只是為了更加方便。
請(qǐng)注意在 {...} 中的 import/export 語句無效。
像這樣的有條件的導(dǎo)入是無效的:
if (something) {
import {sayHi} from "./say.js"; // Error: import must be at top level
}
復(fù)制代碼
55. Proxy 和 Reflect
Proxy 是對(duì)象的包裝器,將代理上的操作轉(zhuǎn)發(fā)到對(duì)象,并可以選擇捕獲其中一些操作。
它可以包裝任何類型的對(duì)象,包括類和函數(shù)。
語法為:
let proxy = new Proxy(target, {
/* trap */
});
復(fù)制代碼
……然后,我們應(yīng)該在所有地方使用 proxy 而不是 target。代理沒有自己的屬性或方法。如果提供了捕捉器(trap),它將捕獲操作,否則會(huì)將其轉(zhuǎn)發(fā)給 target 對(duì)象。
我們可以捕獲:
讀取(get),寫入(set),刪除(deleteProperty)屬性(甚至是不存在的屬性)。
函數(shù)調(diào)用(apply 捕捉器)。
new 操作(construct 捕捉器)。
許多其他操作(完整列表請(qǐng)見本文開頭部分和 docs[41])。
這使我們能夠創(chuàng)建“虛擬”屬性和方法,實(shí)現(xiàn)默認(rèn)值,可觀察對(duì)象,函數(shù)裝飾器等。
我們還可以將對(duì)象多次包裝在不同的代理中,并用多個(gè)各個(gè)方面的功能對(duì)其進(jìn)行裝飾。
Reflect[42] API 旨在補(bǔ)充 Proxy[43]。對(duì)于任意 Proxy 捕捉器,都有一個(gè)帶有相同參數(shù)的 Reflect 調(diào)用。我們應(yīng)該使用它們將調(diào)用轉(zhuǎn)發(fā)給目標(biāo)對(duì)象。
Proxy 有一些局限性:
內(nèi)建對(duì)象具有“內(nèi)部插槽”,對(duì)這些對(duì)象的訪問無法被代理。請(qǐng)參閱上文中的解決方法。
私有類字段也是如此,因?yàn)樗鼈円彩窃趦?nèi)部使用插槽實(shí)現(xiàn)的。因此,代理方法的調(diào)用必須具有目標(biāo)對(duì)象作為 this 才能訪問它們。
對(duì)象的嚴(yán)格相等性檢查 === 無法被攔截。
性能:基準(zhǔn)測(cè)試(benchmark)取決于引擎,但通常使用最簡單的代理訪問屬性所需的時(shí)間也要長幾倍。實(shí)際上,這僅對(duì)某些“瓶頸”對(duì)象來說才重要。
56. 遍歷 DOM
給定一個(gè) DOM 節(jié)點(diǎn),我們可以使用導(dǎo)航(navigation)屬性訪問其直接的鄰居。
這些屬性主要分為兩組:
對(duì)于所有節(jié)點(diǎn):parentNode,childNodes,firstChild,lastChild,previousSibling,nextSibling。
僅對(duì)于元素節(jié)點(diǎn):parentElement,children,firstElementChild,lastElementChild,previousElementSibling,nextElementSibling。
某些類型的 DOM 元素,例如 table,提供了用于訪問其內(nèi)容的其他屬性和集合。
57. 搜索:getElement*,querySelector*
有 6 種主要的方法,可以在 DOM 中搜素節(jié)點(diǎn):

此外:目前為止,最常用的是 querySelector 和 querySelectorAll,但是 getElement(s)By* 可能會(huì)偶爾有用,或者可以在舊腳本中找到。
elem.matches(css) 用于檢查 elem 與給定的 CSS 選擇器是否匹配。
elem.closest(css) 用于查找與給定 CSS 選擇器相匹配的最近的祖先。elem 本身也會(huì)被檢查。
讓我們?cè)谶@里提一下另一種用來檢查子級(jí)與父級(jí)之間關(guān)系的方法,因?yàn)樗袝r(shí)很有用:
如果 elemB 在 elemA 內(nèi)(elemA 的后代)或者 elemA==elemB,elemA.contains(elemB) 將返回 true。
58. 節(jié)點(diǎn)屬性:type,tag 和 content
每個(gè) DOM 節(jié)點(diǎn)都屬于一個(gè)特定的類。這些類形成層次結(jié)構(gòu)(hierarchy)。完整的屬性和方法集是繼承的結(jié)果。
主要的 DOM 節(jié)點(diǎn)屬性有:
nodeType我們可以使用它來查看節(jié)點(diǎn)是文本節(jié)點(diǎn)還是元素節(jié)點(diǎn)。它具有一個(gè)數(shù)值型值(numeric value):1 表示元素,3 表示文本節(jié)點(diǎn),其他一些則代表其他節(jié)點(diǎn)類型。只讀。
nodeName/tagName用于元素名,標(biāo)簽名(除了 XML 模式,都要大寫)。對(duì)于非元素節(jié)點(diǎn),nodeName 描述了它是什么。只讀。
innerHTML元素的 HTML 內(nèi)容。可以被修改。
outerHTML元素的完整 HTML。對(duì) elem.outerHTML 的寫入操作不會(huì)觸及 elem 本身。而是在外部上下文中將其替換為新的 HTML。
nodeValue/data非元素節(jié)點(diǎn)(文本、注釋)的內(nèi)容。兩者幾乎一樣,我們通常使用 data。可以被修改。
textContent元素內(nèi)的文本:HTML 減去所有 。寫入文本會(huì)將文本放入元素內(nèi),所有特殊字符和標(biāo)簽均被視為文本。可以安全地插入用戶生成的文本,并防止不必要的 HTML 插入。
hidden當(dāng)被設(shè)置為 true 時(shí),執(zhí)行與 CSS display:none 相同的事。
DOM 節(jié)點(diǎn)還具有其他屬性,具體有哪些屬性則取決于它們的類。例如, 元素(HTMLInputElement)支持 value,type,而 元素(HTMLAnchorElement)則支持 href 等。大多數(shù)標(biāo)準(zhǔn) HTML 特性(attribute)都具有相應(yīng)的 DOM 屬性。
[
59. 特性和屬性(Attributes and properties)
特性(attribute)— 寫在 HTML 中的內(nèi)容。
屬性(property)— DOM 對(duì)象中的內(nèi)容。
簡略的對(duì)比:

elem.hasAttribute(name) — 檢查是否存在這個(gè)特性。操作特性的方法:
elem.getAttribute(name) — 獲取這個(gè)特性值。
elem.setAttribute(name, value) — 設(shè)置這個(gè)特性值。
elem.removeAttribute(name) — 移除這個(gè)特性。
elem.attributes — 所有特性的集合。
在大多數(shù)情況下,最好使用 DOM 屬性。僅當(dāng) DOM 屬性無法滿足開發(fā)需求,并且我們真的需要特性時(shí),才使用特性,例如:
我們需要一個(gè)非標(biāo)準(zhǔn)的特性。但是如果它以 data- 開頭,那么我們應(yīng)該使用 dataset。
我們想要讀取 HTML 中“所寫的”值。對(duì)應(yīng)的 DOM 屬性可能不同,例如 href 屬性一直是一個(gè) 完整的 URL,但是我們想要的是“原始的”值。
60. 修改文檔(document)
創(chuàng)建新節(jié)點(diǎn)的方法:
document.createElement(tag) — 用給定的標(biāo)簽創(chuàng)建一個(gè)元素節(jié)點(diǎn),
document.createTextNode(value) — 創(chuàng)建一個(gè)文本節(jié)點(diǎn)(很少使用),
elem.cloneNode(deep) — 克隆元素,如果 deep==true 則與其后代一起克隆。
插入和移除節(jié)點(diǎn)的方法:
node.append(...nodes or strings) — 在 node 末尾插入,
node.prepend(...nodes or strings) — 在 node 開頭插入,
node.before(...nodes or strings) — 在 node 之前插入,
node.after(...nodes or strings) — 在 node 之后插入,
node.replaceWith(...nodes or strings) — 替換 node。
node.remove() — 移除 node。
文本字符串被“作為文本”插入。
這里還有“舊式”的方法:
parent.appendChild(node)
parent.insertBefore(node, nextSibling)
parent.removeChild(node)
parent.replaceChild(newElem, node)
這些方法都返回 node。
在 html 中給定一些 HTML,elem.insertAdjacentHTML(where, html) 會(huì)根據(jù) where 的值來插入它:
"beforebegin" — 將 html 插入到 elem 前面,
"afterbegin" — 將 html 插入到 elem 的開頭,
"beforeend" — 將 html 插入到 elem 的末尾,
"afterend" — 將 html 插入到 elem 后面。
另外,還有類似的方法,elem.insertAdjacentText 和 elem.insertAdjacentElement,它們會(huì)插入文本字符串和元素,但很少使用。
要在頁面加載完成之前將 HTML 附加到頁面:
document.write(html)
頁面加載完成后,這樣的調(diào)用將會(huì)擦除文檔。多見于舊腳本。
61. 樣式和類
要管理 class,有兩個(gè) DOM 屬性:
className — 字符串值,可以很好地管理整個(gè)類的集合。
classList — 具有 add/remove/toggle/contains 方法的對(duì)象,可以很好地支持單個(gè)類。
要改變樣式:
](https://link.juejin.cn/?target=undefined)
-
style 屬性是具有駝峰(camelCased)樣式的對(duì)象。對(duì)其進(jìn)行讀取和修改與修改 "style" 特性(attribute)中的各個(gè)屬性具有相同的效果。要了解如何應(yīng)用 important 和其他特殊內(nèi)容 — 在 MDN[44] 中有一個(gè)方法列表。
style.cssText 屬性對(duì)應(yīng)于整個(gè) "style" 特性(attribute),即完整的樣式字符串。
要讀取已解析的(resolved)樣式(對(duì)于所有類,在應(yīng)用所有 CSS 并計(jì)算最終值之后):
getComputedStyle(elem, [pseudo]) 返回與 style 對(duì)象類似的,且包含了所有類的對(duì)象。只讀。
62. 元素大小和滾動(dòng)
元素具有以下幾何屬性:
offsetParent — 是最接近的 CSS 定位的祖先,或者是 td,th,table,body。
offsetLeft/offsetTop — 是相對(duì)于 offsetParent 的左上角邊緣的坐標(biāo)。
offsetWidth/offsetHeight — 元素的“外部” width/height,邊框(border)尺寸計(jì)算在內(nèi)。
clientLeft/clientTop — 從元素左上角外角到左上角內(nèi)角的距離。對(duì)于從左到右顯示內(nèi)容的操作系統(tǒng)來說,它們始終是左側(cè)/頂部 border 的寬度。而對(duì)于從右到左顯示內(nèi)容的操作系統(tǒng)來說,垂直滾動(dòng)條在左邊,所以 clientLeft 也包括滾動(dòng)條的寬度。
clientWidth/clientHeight — 內(nèi)容的 width/height,包括 padding,但不包括滾動(dòng)條(scrollbar)。
scrollWidth/scrollHeight — 內(nèi)容的 width/height,就像 clientWidth/clientHeight 一樣,但還包括元素的滾動(dòng)出的不可見的部分。
scrollLeft/scrollTop — 從元素的左上角開始,滾動(dòng)出元素的上半部分的 width/height。
除了 scrollLeft/scrollTop 外,所有屬性都是只讀的。如果我們修改 scrollLeft/scrollTop,瀏覽器會(huì)滾動(dòng)對(duì)應(yīng)的元素。
63. Window 大小和滾動(dòng)
幾何:
文檔可見部分的 width/height(內(nèi)容區(qū)域的 width/height):document.documentElement.clientWidth/clientHeight
整個(gè)文檔的 width/height,其中包括滾動(dòng)出去的部分:
let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight );
滾動(dòng):
讀取當(dāng)前的滾動(dòng):window.pageYOffset/pageXOffset。
更改當(dāng)前的滾動(dòng):
window.scrollTo(pageX,pageY) — 絕對(duì)坐標(biāo),
window.scrollBy(x,y) — 相對(duì)當(dāng)前位置進(jìn)行滾動(dòng),
elem.scrollIntoView(top) — 滾動(dòng)以使 elem 可見(elem 與窗口的頂部/底部對(duì)齊)。
64. 瀏覽器事件簡介
這里有 3 種分配事件處理程序的方式:
HTML 特性(attribute):onclick="..."。
DOM 屬性(property):elem.onclick = function。
方法(method):elem.addEventListener(event, handler[, phase]) 用于添加,removeEventListener 用于移除。
HTML 特性很少使用,因?yàn)?HTML 標(biāo)簽中的 JavaScript 看起來有些奇怪且陌生。而且也不能在里面寫太多代碼。
DOM 屬性用起來還可以,但我們無法為特定事件分配多個(gè)處理程序。在許多場(chǎng)景中,這種限制并不嚴(yán)重。
最后一種方式是最靈活的,但也是寫起來最長的。有少數(shù)事件只能使用這種方式。例如 transtionend 和 DOMContentLoaded(上文中講到了)。addEventListener 也支持對(duì)象作為事件處理程序。在這種情況下,如果發(fā)生事件,則會(huì)調(diào)用 handleEvent 方法。
無論你如何分類處理程序 —— 它都會(huì)將獲得一個(gè)事件對(duì)象作為第一個(gè)參數(shù)。該對(duì)象包含有關(guān)所發(fā)生事件的詳細(xì)信息。
65. 冒泡和捕獲
當(dāng)一個(gè)事件發(fā)生時(shí) —— 發(fā)生該事件的嵌套最深的元素被標(biāo)記為“目標(biāo)元素”(event.target)。
然后,事件從文檔根節(jié)點(diǎn)向下移動(dòng)到 event.target,并在途中調(diào)用分配了 addEventListener(..., true) 的處理程序(true 是 {capture: true} 的一個(gè)簡寫形式)。
然后,在目標(biāo)元素自身上調(diào)用處理程序。
然后,事件從 event.target 冒泡到根,調(diào)用使用 on、HTML 特性(attribute)和沒有第三個(gè)參數(shù)的,或者第三個(gè)參數(shù)為 false/{capture:false} 的 addEventListener 分配的處理程序。
每個(gè)處理程序都可以訪問 event 對(duì)象的屬性:
event.target —— 引發(fā)事件的層級(jí)最深的元素。
event.currentTarget(=this)—— 處理事件的當(dāng)前元素(具有處理程序的元素)
event.eventPhase —— 當(dāng)前階段(capturing=1,target=2,bubbling=3)。
任何事件處理程序都可以通過調(diào)用 event.stopPropagation() 來停止事件,但不建議這樣做,因?yàn)槲覀儾淮_定是否確實(shí)不需要冒泡上來的事件,也許是用于完全不同的事情。
捕獲階段很少使用,通常我們會(huì)在冒泡時(shí)處理事件。這背后有一個(gè)邏輯。
在現(xiàn)實(shí)世界中,當(dāng)事故發(fā)生時(shí),當(dāng)?shù)鼐綍?huì)首先做出反應(yīng)。他們最了解發(fā)生這件事的地方。然后,如果需要,上級(jí)主管部門再進(jìn)行處理。
事件處理程序也是如此。在特定元素上設(shè)置處理程序的代碼,了解有關(guān)該元素最詳盡的信息。特定于 的處理程序可能恰好適合于該 ,這個(gè)處理程序知道關(guān)于該元素的所有信息。所以該處理程序應(yīng)該首先獲得機(jī)會(huì)。然后,它的直接父元素也了解相關(guān)上下文,但了解的內(nèi)容會(huì)少一些,以此類推,直到處理一般性概念并運(yùn)行最后一個(gè)處理程序的最頂部的元素為止。
66. 事件委托
它通常用于為許多相似的元素添加相同的處理,但不僅限于此。
算法:
在容器(container)上放一個(gè)處理程序。
在處理程序中 —— 檢查源元素 event.target。
如果事件發(fā)生在我們感興趣的元素內(nèi),那么處理該事件。
好處:
簡化初始化并節(jié)省內(nèi)存:無需添加許多處理程序。
更少的代碼:添加或移除元素時(shí),無需添加/移除處理程序。
DOM 修改 :我們可以使用 innerHTML 等,來批量添加/移除元素。
事件委托也有其局限性:
首先,事件必須冒泡。而有些事件不會(huì)冒泡。此外,低級(jí)別的處理程序不應(yīng)該使用 event.stopPropagation()。
其次,委托可能會(huì)增加 CPU 負(fù)載,因?yàn)槿萜骷?jí)別的處理程序會(huì)對(duì)容器中任意位置的事件做出反應(yīng),而不管我們是否對(duì)該事件感興趣。但是,通常負(fù)載可以忽略不計(jì),所以我們不考慮它。
67. 瀏覽器默認(rèn)行為
有很多默認(rèn)的瀏覽器行為:
mousedown —— 開始選擇(移動(dòng)鼠標(biāo)進(jìn)行選擇)。
在 上的 click —— 選中/取消選中的 input。
submit —— 點(diǎn)擊 或者在表單字段中按下 Enter 鍵會(huì)觸發(fā)該事件,之后瀏覽器將提交表單。
keydown —— 按下一個(gè)按鍵會(huì)導(dǎo)致將字符添加到字段,或者觸發(fā)其他行為。
contextmenu —— 事件發(fā)生在鼠標(biāo)右鍵單擊時(shí),觸發(fā)的行為是顯示瀏覽器上下文菜單。
……還有更多……
如果我們只想通過 JavaScript 來處理事件,那么所有默認(rèn)行為都是可以被阻止的。
想要阻止默認(rèn)行為 —— 可以使用 event.preventDefault() 或 return false。第二個(gè)方法只適用于通過 on 分配的處理程序。
addEventListener 的 passive: true 選項(xiàng)告訴瀏覽器該行為不會(huì)被阻止。這對(duì)于某些移動(dòng)端的事件(像 touchstart 和 touchmove)很有用,用以告訴瀏覽器在滾動(dòng)之前不應(yīng)等待所有處理程序完成。
如果默認(rèn)行為被阻止,event.defaultPrevented 的值會(huì)變成 true,否則為 false。
68. 創(chuàng)建自定義事件
要從代碼生成一個(gè)事件,我們首先需要?jiǎng)?chuàng)建一個(gè)事件對(duì)象。
通用的 Event(name, options) 構(gòu)造器接受任意事件名稱和具有兩個(gè)屬性的 options 對(duì)象:
如果事件應(yīng)該冒泡,則 bubbles: true。
如果 event.preventDefault() 應(yīng)該有效,則 cancelable: true。
其他像 MouseEvent 和 KeyboardEvent 這樣的原生事件的構(gòu)造器,都接受特定于該事件類型的屬性。例如,鼠標(biāo)事件的 clientX。
對(duì)于自定義事件,我們應(yīng)該使用 CustomEvent 構(gòu)造器。它有一個(gè)名為 detail 的附加選項(xiàng),我們應(yīng)該將事件特定的數(shù)據(jù)分配給它。然后,所有處理程序可以以 event.detail 的形式來訪問它。
盡管技術(shù)上可以生成像 click 或 keydown 這樣的瀏覽器事件,但我們還是應(yīng)謹(jǐn)慎使用它們。
我們不應(yīng)該生成瀏覽器事件,因?yàn)檫@是運(yùn)行處理程序的一種怪異(hacky)方式。大多數(shù)時(shí)候,這都是糟糕的架構(gòu)。
可以生成原生事件:
如果第三方程序庫不提供其他交互方式,那么這是使第三方程序庫工作所需的一種骯臟手段。
對(duì)于自動(dòng)化測(cè)試,要在腳本中“點(diǎn)擊按鈕”并查看接口是否正確響應(yīng)。
使用我們自己的名稱的自定義事件通常是出于架構(gòu)的目的而創(chuàng)建的,以指示發(fā)生在菜單(menu),滑塊(slider),輪播(carousel)等內(nèi)部發(fā)生了什么。
69. 鼠標(biāo)事件
鼠標(biāo)事件有以下屬性:
按鈕:button。
組合鍵(如果被按下則為 true):altKey,ctrlKey,shiftKey 和 metaKey(Mac)。
如果你想處理 Ctrl,那么不要忘記 Mac 用戶,他們通常使用的是 Cmd,所以最好檢查 if (e.metaKey || e.ctrlKey)。 窗口相對(duì)坐標(biāo):clientX/clientY。
文檔相對(duì)坐標(biāo):pageX/pageY。
mousedown 的默認(rèn)瀏覽器操作是文本選擇,如果它對(duì)界面不利,則應(yīng)避免它。
70. 移動(dòng)鼠標(biāo):mouseover/out,mouseenter/leave
以下這些內(nèi)容要注意:
快速移動(dòng)鼠標(biāo)可能會(huì)跳過中間元素。
mouseover/out 和 mouseenter/leave 事件還有一個(gè)附加屬性:relatedTarget。這就是我們來自/到的元素,是對(duì) target 的補(bǔ)充。
即使我們從父元素轉(zhuǎn)到子元素時(shí),也會(huì)觸發(fā) mouseover/out 事件。瀏覽器假定鼠標(biāo)一次只會(huì)位于一個(gè)元素上 —— 最深的那個(gè)。
mouseenter/leave 事件在這方面不同:它們僅在鼠標(biāo)進(jìn)入和離開元素時(shí)才觸發(fā)。并且它們不會(huì)冒泡。
71. 事件:change,input,cut,copy,paste
數(shù)據(jù)更改事件:

關(guān)于本文
作者:JayYuen
https://juejin.cn/post/6985459853183434789


“分享、點(diǎn)贊、在看” 支持一波 
