深入 JavaScript
請支持這本書:購買捐款
(廣告,請不要阻擋。)

3 解構演算法



在本章中,我們將從不同的角度來看解構:作為遞迴模式比對演算法。

演算法將讓我們更了解預設值。這在最後會很有用,我們將嘗試找出以下兩個函式的差異

function move({x=0, y=0} = {})         { ··· }
function move({x, y} = { x: 0, y: 0 }) { ··· }

3.1 準備模式比對演算法

解構指定看起來像這樣

«pattern» = «value»

我們要使用 patternvalue 中擷取資料。

現在我們將探討一個執行這種指定演算法。此演算法在函數式程式設計中稱為模式比對(簡稱:比對)。它指定運算子 (「比對」),用於將 patternvalue 比對,並在執行此操作時指定變數

«pattern» ← «value»

我們只會探討解構指定,但解構變數宣告和解構參數定義的工作方式類似。我們也不會探討進階功能:計算屬性鍵、屬性值速記,以及物件屬性和陣列元素作為指定目標,超出了本章的範圍。

匹配運算子的規格包含宣告式規則,這些規則會深入兩個運算元的結構。宣告式符號可能需要一些時間適應,但它讓規格更簡潔。

3.1.1 使用宣告式規則來指定比對演算法

本章中使用的宣告式規則會對輸入進行運算,並透過副作用產生演算法的結果。以下是一個這樣的規則(我們稍後會再看到)

此規則包含以下部分

在規則 (2c) 中,標頭表示如果物件範本至少有一個屬性(其金鑰為 key)和零個或更多個其餘屬性,則可以套用此規則。此規則的效果是執行持續進行,屬性值範本與 obj.key 相符,其餘屬性與 obj 相符。

讓我們考慮本章中的另一條規則

在規則 (2e) 中,標頭表示如果空物件範本 {} 與值 obj 相符,則執行此規則。主體表示,在這種情況下,我們完成了。

規則 (2c) 和規則 (2e) 共同形成一個宣告式迴圈,這個迴圈會反覆運算箭頭左側範本的屬性。

3.1.2 根據宣告式規則評估表達式

完整的演算法透過宣告式規則的順序指定。假設我們想要評估以下比對表達式

{first: f, last: l} ← obj

若要套用規則順序,我們從上到下檢閱這些規則,並執行第一個適用的規則。如果在該規則的主體中有比對表達式,則再次套用這些規則。以此類推。

有時標頭會包含一個條件,這個條件也會決定規則是否適用,例如

3.2 範本比對演算法

3.2.1 範本

範本可以是

接下來的三個區段指定在匹配表達式中處理這三種情況的規則。

3.2.2 變數規則

3.2.3 物件模式規則

規則 2a 和 2b 處理非法值。規則 2c–2e 迴圈模式的屬性。在規則 2d 中,我們可以看到預設值提供一個替代方案,如果在 obj 中沒有匹配的屬性,則與之匹配。

3.2.4 陣列模式規則

陣列模式和可迭代。陣列解構的演算法從陣列模式和可迭代開始

輔助函式

function isIterable(value) {
  return (value !== null
    && typeof value === 'object'
    && typeof value[Symbol.iterator] === 'function');
}

陣列元素和迭代器。演算法繼續

這些是規則

輔助函式

function getNext(iterator) {
  const {done,value} = iterator.next();
  return (done ? undefined : value);
}

迭代器完成類似於物件中缺少屬性。

3.3 空物件模式和陣列模式

演算法規則的有趣結果:我們可以用空物件模式和空陣列模式解構。

給定一個空物件模式 {}:如果要解構的值既不是 undefined 也不是 null,則什麼都不會發生。否則,會擲出 TypeError

const {} = 123; // OK, neither undefined nor null
assert.throws(
  () => {
    const {} = null;
  },
  /^TypeError: Cannot destructure 'null' as it is null.$/)

給定一個空陣列模式 []:如果要解構的值是可迭代的,則什麼都不會發生。否則,會擲出 TypeError

const [] = 'abc'; // OK, iterable
assert.throws(
  () => {
    const [] = 123; // not iterable
  },
  /^TypeError: 123 is not iterable$/)

換句話說:空解構模式強制值具備某些特徵,但沒有其他影響。

3.4 套用演算法

在 JavaScript 中,命名參數是透過物件模擬的:呼叫者使用物件文字,而被呼叫者使用解構。這個模擬在「JavaScript for impatient programmers」中有詳細的說明。以下程式碼顯示一個範例:函式 move1() 有兩個命名參數,xy

function move1({x=0, y=0} = {}) { // (A)
  return [x, y];
}
assert.deepEqual(
  move1({x: 3, y: 8}), [3, 8]);
assert.deepEqual(
  move1({x: 3}), [3, 0]);
assert.deepEqual(
  move1({}), [0, 0]);
assert.deepEqual(
  move1(), [0, 0]);

A 行中有三個預設值

但是,為什麼我們要在前一個程式碼片段中定義參數?為什麼不如下面這樣呢?

function move2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

為了了解為什麼 move1() 是正確的,我們將在兩個範例中使用這兩個函式。在我們這麼做之前,讓我們看看如何透過比對來解釋參數的傳遞。

3.4.1 背景:透過比對傳遞參數

對於函式呼叫,形式參數(在函式定義中)會與實際參數(在函式呼叫中)進行比對。舉例來說,請看以下函式定義和函式呼叫。

function func(a=0, b=0) { ··· }
func(1, 2);

參數 ab 的設定類似於以下解構。

[a=0, b=0] ← [1, 2]

3.4.2 使用 move2()

讓我們檢查解構如何適用於 move2()

範例 1. 函式呼叫 move2() 會導致這個解構

[{x, y} = { x: 0, y: 0 }] ← []

左側的單一陣列元素在右側沒有比對,這就是為什麼 {x,y} 會與預設值比對,而不是與右側的資料比對(規則 3b、3d)

{x, y} ← { x: 0, y: 0 }

左側包含屬性值簡寫。它是以下內容的縮寫

{x: x, y: y} ← { x: 0, y: 0 }

這個解構會導致以下兩個指定(規則 2c、1)

x = 0;
y = 0;

這就是我們想要的。然而,在下一則範例中,我們就沒那麼幸運了。

範例 2. 讓我們檢查函式呼叫 move2({z: 3}),它會導致以下解構

[{x, y} = { x: 0, y: 0 }] ← [{z: 3}]

右側在索引 0 處有一個陣列元素。因此,預設值會被忽略,而下一步是(規則 3d)

{x, y} ← { z: 3 }

這會導致 xy 都設定為 undefined,這不是我們想要的。問題在於 {x,y} 不再與預設值比對,而是與 {z:3} 比對。

3.4.3 使用 move1()

我們來試試 move1()

範例 1: move1()

[{x=0, y=0} = {}] ← []

我們在右手邊沒有索引為 0 的陣列元素,並使用預設值(規則 3d)

{x=0, y=0} ← {}

左手邊包含屬性值速記,這表示此解構等於

{x: x=0, y: y=0} ← {}

屬性 x 和屬性 y 都不在右手邊有匹配項。因此,預設值會被使用,並執行以下解構(規則 2d)

x ← 0
y ← 0

這會導致以下指定(規則 1)

x = 0
y = 0

在這裡,我們得到了想要的結果。讓我們看看在下一範例中,我們的運氣是否持續。

範例 2: move1({z: 3})

[{x=0, y=0} = {}] ← [{z: 3}]

陣列樣式的第一個元素在右手邊有匹配項,而該匹配項會用來繼續解構(規則 3d)

{x=0, y=0} ← {z: 3}

如同範例 1,在右手邊沒有屬性 xy,且預設值會被使用

x = 0
y = 0

它運作如預期!這次,樣式包含 xy 匹配 {z:3} 沒有問題,因為它們有自己的區域預設值。

3.4.4 結論:預設值是樣式部分的特色

這些範例顯示預設值是樣式部分(物件屬性或陣列元素)的特色。如果一個部分沒有匹配項或匹配到 undefined,則會使用預設值。也就是說,樣式會匹配到預設值。