<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試前必備的 JavaScript 基礎(chǔ)知識(shí)梳理總結(jié)

          共 39116字,需瀏覽 79分鐘

           ·

          2021-08-10 06:21

          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)方便的。它具體有兩種:

          1. 不帶花括號(hào):(...args) => expression — 右側(cè)是一個(gè)表達(dá)式:函數(shù)計(jì)算表達(dá)式并返回其結(jié)果。

          2. 帶花括號(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. 可選鏈 "?."

          可選鏈 ?. 語法有三種形式:

          1. obj?.prop —— 如果 obj 存在則返回 obj.prop,否則返回 undefined。

          2. obj?.[prop] —— 如果 obj 存在則返回 obj[prop],否則返回 undefined。

          3. 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)景:

          1. “隱藏” 對(duì)象屬性。如果我們想要向“屬于”另一個(gè)腳本或者庫的對(duì)象添加一個(gè)屬性,我們可以創(chuàng)建一個(gè) Symbol 并使用它作為屬性的鍵。Symbol 屬性不會(huì)出現(xiàn)在 for..in 中,因此它不會(huì)意外地被與其他屬性一起處理。并且,它不會(huì)被直接訪問,因?yàn)榱硪粋€(gè)腳本沒有我們的 symbol。因此,該屬性將受到保護(hù),防止被意外使用或重寫。

          因此我們可以使用 Symbol 屬性“秘密地”將一些東西隱藏到我們需要的對(duì)象中,但其他地方看不到它。

          1. 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. 類繼承

          1. 想要擴(kuò)展一個(gè)類:class Child extends Parent:
          • 這意味著 Child.prototype.__proto__ 將是 Parent.prototype,所以方法會(huì)被繼承。
          1. 重寫一個(gè) constructor:
          • 在使用 this 之前,我們必須在 Child 的 constructor 中將父 constructor 調(diào)用為 super()。
          1. 重寫一個(gè)方法:
          • 我們可以在一個(gè) Child 方法中使用 super.method() 來調(diào)用 Parent 方法。
          1. 內(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ù)制代碼
          1. 我們?yōu)?HTTP 錯(cuò)誤創(chuàng)建一個(gè)自定義類用于區(qū)分 HTTP 錯(cuò)誤和其他類型錯(cuò)誤。此外,新的類有一個(gè) constructor,它接受 response 對(duì)象,并將其保存到 error 中。因此,錯(cuò)誤處理(error-handling)代碼就能夠獲得響應(yīng)數(shù)據(jù)了。

          2. 然后我們將請(qǐng)求(requesting)和錯(cuò)誤處理代碼包裝進(jìn)一個(gè)函數(shù),它能夠 fetch url 并 將所有狀態(tài)碼不是 200 視為錯(cuò)誤。這很方便,因?yàn)槲覀兺ǔP枰@樣的邏輯。

          3. 現(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)方法:

          1. Promise.all(promises) —— 等待所有 promise 都 resolve 時(shí),返回存放它們結(jié)果的數(shù)組。如果給定的任意一個(gè) promise 為 reject,那么它就會(huì)變成 Promise.all 的 error,所有其他 promise 的結(jié)果都會(huì)被忽略。

          2. Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 時(shí),并以包含以下內(nèi)容的對(duì)象數(shù)組的形式返回它們的結(jié)果:

            • status: "fulfilled" 或 "rejected"

            • value(如果 fulfilled)或 reason(如果 rejected)。

          3. Promise.race(promises) —— 等待第一個(gè) settle 的 promise,并將其 result/error 作為結(jié)果。

          4. Promise.resolve(value) —— 使用給定 value 創(chuàng)建一個(gè) resolved 的 promise。

          5. 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è)作用:

          1. 讓這個(gè)函數(shù)總是返回一個(gè) promise。

          2. 允許在該函數(shù)內(nèi)使用 await。

          Promise 前的關(guān)鍵字 await 使 JavaScript 引擎等待該 promise settle,然后:

          1. 如果有 error,就會(huì)拋出異常 — 就像那里調(diào)用了 throw error 一樣。

          2. 否則,就返回結(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é)一下模塊的核心概念:

          1. 一個(gè)模塊就是一個(gè)文件。瀏覽器需要使用
          • 默認(rèn)是延遲解析的(deferred)。

          • Async 可用于內(nèi)聯(lián)腳本。

          • 要從另一個(gè)源(域/協(xié)議/端口)加載外部腳本,需要 CORS header。

          • 重復(fù)的外部腳本會(huì)被忽略

          1. 模塊具有自己的本地頂級(jí)作用域,并可以通過 import/export 交換功能。

          2. 模塊始終使用 use strict。

          3. 模塊代碼只執(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 種分配事件處理程序的方式:

          1. HTML 特性(attribute):onclick="..."。

          2. DOM 屬性(property):elem.onclick = function。

          3. 方法(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. 事件委托

          它通常用于為許多相似的元素添加相同的處理,但不僅限于此。

          算法:

          1. 在容器(container)上放一個(gè)處理程序。

          2. 在處理程序中 —— 檢查源元素 event.target。

          3. 如果事件發(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


          如果覺得這篇文章還不錯(cuò)
          點(diǎn)擊下面卡片關(guān)注我

          來個(gè)【分享、點(diǎn)贊、在看】三連支持一下吧

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

          瀏覽 21
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久国产一卡二卡 | 婷婷综合无码 | 黄在线免费观看视频 | 91精品国自产欧美在线观看 | 亚洲无码成人视频在线观看 |