move2()
move1()
在本章中,我們將從不同的角度來看解構:作為遞迴模式比對演算法。
演算法將讓我們更了解預設值。這在最後會很有用,我們將嘗試找出以下兩個函式的差異
解構指定看起來像這樣
我們要使用 pattern
從 value
中擷取資料。
現在我們將探討一個執行這種指定演算法。此演算法在函數式程式設計中稱為模式比對(簡稱:比對)。它指定運算子 ←
(「比對」),用於將 pattern
與 value
比對,並在執行此操作時指定變數
我們只會探討解構指定,但解構變數宣告和解構參數定義的工作方式類似。我們也不會探討進階功能:計算屬性鍵、屬性值速記,以及物件屬性和陣列元素作為指定目標,超出了本章的範圍。
匹配運算子的規格包含宣告式規則,這些規則會深入兩個運算元的結構。宣告式符號可能需要一些時間適應,但它讓規格更簡潔。
本章中使用的宣告式規則會對輸入進行運算,並透過副作用產生演算法的結果。以下是一個這樣的規則(我們稍後會再看到)
(2c) {key: «pattern», «properties»} ← obj
此規則包含以下部分
在規則 (2c) 中,標頭表示如果物件範本至少有一個屬性(其金鑰為 key
)和零個或更多個其餘屬性,則可以套用此規則。此規則的效果是執行持續進行,屬性值範本與 obj.key
相符,其餘屬性與 obj
相符。
讓我們考慮本章中的另一條規則
(2e) {} ← obj
(沒有剩餘屬性)
在規則 (2e) 中,標頭表示如果空物件範本 {}
與值 obj
相符,則執行此規則。主體表示,在這種情況下,我們完成了。
規則 (2c) 和規則 (2e) 共同形成一個宣告式迴圈,這個迴圈會反覆運算箭頭左側範本的屬性。
完整的演算法透過宣告式規則的順序指定。假設我們想要評估以下比對表達式
{first: f, last: l} ← obj
若要套用規則順序,我們從上到下檢閱這些規則,並執行第一個適用的規則。如果在該規則的主體中有比對表達式,則再次套用這些規則。以此類推。
有時標頭會包含一個條件,這個條件也會決定規則是否適用,例如
(3a) [«elements»] ← non_iterable
if (!isIterable(non_iterable))
範本可以是
x
{«properties»}
[«元素»]
接下來的三個區段指定在匹配表達式中處理這三種情況的規則。
x ← 值
(包括 undefined
和 null
)(2a) {«屬性»} ← undefined
(非法值)
(2b) {«屬性»} ← null
(非法值)
(2c) {key: «pattern», «properties»} ← obj
(2d) {key: «模式» = 預設值, «屬性»} ← obj
(2e) {} ← obj
(沒有剩餘屬性)
規則 2a 和 2b 處理非法值。規則 2c–2e 迴圈模式的屬性。在規則 2d 中,我們可以看到預設值提供一個替代方案,如果在 obj
中沒有匹配的屬性,則與之匹配。
陣列模式和可迭代。陣列解構的演算法從陣列模式和可迭代開始
(3a) [«元素»] ← 非可迭代
(非法值)
if (!isIterable(non_iterable))
(3b) [«元素»] ← 可迭代
如果 (isIterable(可迭代))
輔助函式
function isIterable(value) {
return (value !== null
&& typeof value === 'object'
&& typeof value[Symbol.iterator] === 'function');
}
陣列元素和迭代器。演算法繼續
這些是規則
(3c) «模式», «元素» ← 迭代器
(3d) «模式» = 預設值, «元素» ← 迭代器
(3e) , «元素» ← 迭代器
(洞,省略)
(3f) ...«模式» ← 迭代器
(總是最後一部分!)
(3g) ← 迭代器
(沒有元素剩餘)
輔助函式
function getNext(iterator) {
const {done,value} = iterator.next();
return (done ? undefined : value);
}
迭代器完成類似於物件中缺少屬性。
演算法規則的有趣結果:我們可以用空物件模式和空陣列模式解構。
給定一個空物件模式 {}
:如果要解構的值既不是 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$/)
換句話說:空解構模式強制值具備某些特徵,但沒有其他影響。
在 JavaScript 中,命名參數是透過物件模擬的:呼叫者使用物件文字,而被呼叫者使用解構。這個模擬在「JavaScript for impatient programmers」中有詳細的說明。以下程式碼顯示一個範例:函式 move1()
有兩個命名參數,x
和 y
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 行中有三個預設值
x
和 y
。move1()
(就像最後一行)。但是,為什麼我們要在前一個程式碼片段中定義參數?為什麼不如下面這樣呢?
為了了解為什麼 move1()
是正確的,我們將在兩個範例中使用這兩個函式。在我們這麼做之前,讓我們看看如何透過比對來解釋參數的傳遞。
對於函式呼叫,形式參數(在函式定義中)會與實際參數(在函式呼叫中)進行比對。舉例來說,請看以下函式定義和函式呼叫。
參數 a
和 b
的設定類似於以下解構。
move2()
讓我們檢查解構如何適用於 move2()
。
範例 1. 函式呼叫 move2()
會導致這個解構
左側的單一陣列元素在右側沒有比對,這就是為什麼 {x,y}
會與預設值比對,而不是與右側的資料比對(規則 3b、3d)
左側包含屬性值簡寫。它是以下內容的縮寫
這個解構會導致以下兩個指定(規則 2c、1)
這就是我們想要的。然而,在下一則範例中,我們就沒那麼幸運了。
範例 2. 讓我們檢查函式呼叫 move2({z: 3})
,它會導致以下解構
右側在索引 0 處有一個陣列元素。因此,預設值會被忽略,而下一步是(規則 3d)
這會導致 x
和 y
都設定為 undefined
,這不是我們想要的。問題在於 {x,y}
不再與預設值比對,而是與 {z:3}
比對。
move1()
我們來試試 move1()
。
範例 1: move1()
我們在右手邊沒有索引為 0 的陣列元素,並使用預設值(規則 3d)
左手邊包含屬性值速記,這表示此解構等於
屬性 x
和屬性 y
都不在右手邊有匹配項。因此,預設值會被使用,並執行以下解構(規則 2d)
這會導致以下指定(規則 1)
在這裡,我們得到了想要的結果。讓我們看看在下一範例中,我們的運氣是否持續。
範例 2: move1({z: 3})
陣列樣式的第一個元素在右手邊有匹配項,而該匹配項會用來繼續解構(規則 3d)
如同範例 1,在右手邊沒有屬性 x
和 y
,且預設值會被使用
它運作如預期!這次,樣式包含 x
和 y
匹配 {z:3}
沒有問題,因為它們有自己的區域預設值。
這些範例顯示預設值是樣式部分(物件屬性或陣列元素)的特色。如果一個部分沒有匹配項或匹配到 undefined
,則會使用預設值。也就是說,樣式會匹配到預設值。