10. 解構
目錄
請支持這本書:購買(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖。)

10. 解構



10.1 概述

解構是一種從儲存在(可能巢狀的)物件和陣列中的資料中萃取多個值的方法。它可以用於接收資料的位置(例如指定式的左側)。如何萃取值是透過模式指定的(請繼續閱讀以取得範例)。

10.1.1 物件解構

解構物件

const obj = { first: 'Jane', last: 'Doe' };
const {first: f, last: l} = obj;
    // f = 'Jane'; l = 'Doe'

// {prop} is short for {prop: prop}
const {first, last} = obj;
    // first = 'Jane'; last = 'Doe'

解構有助於處理回傳值

const obj = { foo: 123 };

const {writable, configurable} =
    Object.getOwnPropertyDescriptor(obj, 'foo');

console.log(writable, configurable); // true true

10.1.2 陣列解構

陣列解構(適用於所有可迭代值)

const iterable = ['a', 'b'];
const [x, y] = iterable;
    // x = 'a'; y = 'b'

解構有助於處理回傳值

const [all, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

10.1.3 解構可以在哪些地方使用?

解構可以在以下位置使用(我展示陣列模式來示範;物件模式也能正常運作)

// Variable declarations:
const [x] = ['a'];
let [x] = ['a'];
var [x] = ['a'];

// Assignments:
[x] = ['a'];

// Parameter definitions:
function f([x]) { ··· }
f(['a']);

您也可以在 for-of 迴圈中解構

const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
    console.log(index, element);
}
// Output:
// 0 a
// 1 b

10.2 背景:建構資料與萃取資料

為了完全了解解構是什麼,讓我們先檢視其更廣泛的背景。

JavaScript 有用於建構資料的運算,一次一個屬性

const obj = {};
obj.first = 'Jane';
obj.last = 'Doe';

相同的語法可以用於萃取資料。同樣地,一次一個屬性

const f = obj.first;
const l = obj.last;

此外,還有用於一次建構多個屬性的語法,透過物件文字

const obj = { first: 'Jane', last: 'Doe' };

在 ES6 之前,沒有對應的機制來萃取資料。這就是解構的用途,它讓您可以透過物件模式從物件中萃取多個屬性。例如,在指定運算的左手邊

const { first: f, last: l } = obj;

您也可以透過模式解構陣列

const [x, y] = ['a', 'b']; // x = 'a'; y = 'b'

10.3 解構的模式

解構涉及以下兩個部分

解構目標是下列三種模式之一

這表示您可以任意深度巢狀模式

const obj = { a: [{ foo: 123, bar: 'abc' }, {}], b: true };
const { a: [{foo: f}] } = obj; // f = 123

10.3.1 挑選您需要的

如果您解構物件,您只會提到您有興趣的屬性

const { x: x } = { x: 7, y: 3 }; // x = 7

如果您解構陣列,您可以選擇只萃取一個前綴

const [x,y] = ['a', 'b', 'c']; // x='a'; y='b';

10.4 模式如何存取值的內部?

在指定 pattern = someValue 時,pattern 如何存取 someValue 中的內容?

10.4.1 物件樣式會將值轉換為物件

物件樣式會在存取屬性之前,將解構來源轉換為物件。這表示它適用於基本值

const {length : len} = 'abc'; // len = 3
const {toString: s} = 123; // s = Number.prototype.toString
10.4.1.1 無法將值解構為物件

轉換為物件並非透過 Object() 執行,而是透過內部運算 ToObject() 執行。這兩個運算處理 undefinednull 的方式不同。

Object() 會將基本值轉換為包裝物件,並保留物件不變

> typeof Object('abc')
'object'

> var obj = {};
> Object(obj) === obj
true

它也會將 undefinednull 轉換為空物件

> Object(undefined)
{}
> Object(null)
{}

相反地,ToObject() 如果遇到 undefinednull,就會擲回 TypeError。因此,下列解構會失敗,甚至在解構存取任何屬性之前

const { prop: x } = undefined; // TypeError
const { prop: y } = null; // TypeError

因此,您可以使用空物件樣式 {} 來檢查值是否可轉換為物件。正如我們所見,只有 undefinednull 不可轉換

({} = [true, false]); // OK, Arrays are coercible to objects
({} = 'abc'); // OK, strings are coercible to objects

({} = undefined); // TypeError
({} = null); // TypeError

表達式周圍的括號是必要的,因為陳述式在 JavaScript 中不能以大括號開頭 (稍後會說明詳細資訊).

10.4.2 陣列樣式適用於可迭代物件

陣列解構使用迭代器來取得來源的元素。因此,您可以對任何可迭代值執行陣列解構。我們來看可迭代值的範例。

字串是可迭代的

const [x,...y] = 'abc'; // x='a'; y=['b', 'c']

別忘了字串的迭代器會傳回碼點(「Unicode 字元」,21 位元),而不是碼元(「JavaScript 字元」,16 位元)。(有關 Unicode 的更多資訊,請參閱「Speaking JavaScript」中的「第 24 章 Unicode 和 JavaScript」章節。)例如

const [x,y,z] = 'a\uD83D\uDCA9c'; // x='a'; y='\uD83D\uDCA9'; z='c'

您無法透過索引存取 Set 的元素,但您可以透過迭代器存取。因此,陣列解構適用於 Set

const [x,y] = new Set(['a', 'b']); // x='a'; y='b’;

Set 迭代器總是按插入順序傳回元素,這就是前一個解構的結果總是相同的緣故。

10.4.2.1 無法陣列解構一個值

如果一個值有一個方法,其金鑰為 `Symbol.iterator`,且會傳回一個物件,則該值為可迭代的。如果要解構的值不可迭代,陣列解構會擲出 `TypeError`

let x;
[x] = [true, false]; // OK, Arrays are iterable
[x] = 'abc'; // OK, strings are iterable
[x] = { * [Symbol.iterator]() { yield 1 } }; // OK, iterable

[x] = {}; // TypeError, empty objects are not iterable
[x] = undefined; // TypeError, not iterable
[x] = null; // TypeError, not iterable

即使在存取可迭代元素之前,也會擲出 `TypeError`,這表示你可以使用空的陣列模式 `[]` 來檢查一個值是否可迭代

[] = {}; // TypeError, empty objects are not iterable
[] = undefined; // TypeError, not iterable
[] = null; // TypeError, not iterable

10.5 預設值

預設值 是模式的選用功能。如果在來源中找不到任何值,它們會提供一個後備值。如果某個部分(一個物件屬性或一個陣列元素)在來源中沒有對應項,它會與下列項目配對

我們來看一個範例。在以下解構中,索引 0 處的元素在右手邊沒有對應項。因此,解構會繼續進行,將 `x` 與 3 配對,這會導致 `x` 被設定為 3。

const [x=3, y] = []; // x = 3; y = undefined

你也可以在物件模式中使用預設值

const {foo: x=3, bar: y} = {}; // x = 3; y = undefined

10.5.1 undefined 會觸發預設值

如果某個部分確實有一個對應項,但該對應項是 `undefined`,也會使用預設值

const [x=1] = [undefined]; // x = 1
const {prop: y=2} = {prop: undefined}; // y = 2

這種行為的理由在下一章中說明,請參閱 參數預設值

10.5.2 預設值會依需求計算

預設值本身只會在需要時計算。換句話說,這個解構

const {prop: y=someFunc()} = someValue;

等同於

let y;
if (someValue.prop === undefined) {
    y = someFunc();
} else {
    y = someValue.prop;
}

你可以觀察,如果你使用 `console.log()`

> function log(x) { console.log(x); return 'YES' }

> const [a=log('hello')] = [];
> a
'YES'

> const [b=log('hello')] = [123];
> b
123

在第二個解構中,預設值不會觸發,也不會呼叫 `log()`。

10.5.3 預設值可以參照模式中的其他變數

預設值可以參照任何變數,包括同一個模式中的其他變數

const [x=3, y=x] = [];     // x=3; y=3
const [x=3, y=x] = [7];    // x=7; y=7
const [x=3, y=x] = [7, 2]; // x=7; y=2

但是,順序很重要:變數 `x` 和 `y` 從左到右宣告,如果在宣告之前存取它們,會產生 `ReferenceError`

const [x=y, y=3] = []; // ReferenceError

10.5.4 模式的預設值

到目前為止,我們只看過變數的預設值,但你也可以將預設值與模式關聯

const [{ prop: x } = {}] = [];

這代表什麼意思?回想預設值的規則:如果某個部分在來源中沒有配對,解構會繼續使用預設值。

索引 0 的元素沒有配對,這就是解構繼續使用

const { prop: x } = {}; // x = undefined

如果你將模式 { prop: x } 替換成變數 pattern,你會更容易看出為什麼會這樣。

const [pattern = {}] = [];

10.5.5 更複雜的預設值

讓我們進一步探討模式的預設值。在以下範例中,我們透過預設值 { prop: 123 }x 指定一個值

const [{ prop: x } = { prop: 123 }] = [];

由於索引 0 的陣列元素在右手邊沒有配對,因此解構繼續如下,而 x 設為 123。

const { prop: x } = { prop: 123 };  // x = 123

不過,如果右手邊有索引 0 的元素,則 x 不會以這種方式指定值,因為這樣不會觸發預設值。

const [{ prop: x } = { prop: 123 }] = [{}];

這種情況下,解構繼續使用

const { prop: x } = {}; // x = undefined

因此,如果你想要在物件或屬性遺失時讓 x 為 123,你需要為 x 本身指定一個預設值

const [{ prop: x=123 } = {}] = [{}];

在此,解構繼續如下,而與右手邊是 [{}][] 無關。

const { prop: x=123 } = {}; // x = 123

10.6 更多物件解構功能

10.6.1 屬性值簡寫

屬性值簡寫是物件文字的一個功能:如果屬性值是與屬性鍵同名的變數,則你可以省略鍵。這也適用於解構

const { x, y } = { x: 11, y: 8 }; // x = 11; y = 8

// Same as:
const { x: x, y: y } = { x: 11, y: 8 };

你也可以將屬性值簡寫與預設值結合使用

const { x, y = 1 } = {}; // x = undefined; y = 1

10.6.2 運算屬性鍵

運算屬性鍵是另一個物件文字功能,也適用於解構。如果你將運算式放在方括號中,你可以透過運算式指定屬性的鍵

const FOO = 'foo';
const { [FOO]: f } = { foo: 123 }; // f = 123

運算屬性鍵允許你解構鍵為符號的屬性

// Create and destructure a property whose key is a symbol
const KEY = Symbol();
const obj = { [KEY]: 'abc' };
const { [KEY]: x } = obj; // x = 'abc'

// Extract Array.prototype[Symbol.iterator]
const { [Symbol.iterator]: func } = [];
console.log(typeof func); // function

10.7 更多陣列解構功能

10.7.1 省略

省略讓您使用陣列「洞」的語法,在解構過程中跳過元素

const [,, x, y] = ['a', 'b', 'c', 'd']; // x = 'c'; y = 'd'

10.7.2 剩餘運算子 (...)

剩餘運算子讓您將可迭代物件的剩餘元素萃取到陣列中。如果這個運算子用在陣列模式中,它必須放在最後

const [x, ...y] = ['a', 'b', 'c']; // x='a'; y=['b', 'c']

如果運算子找不到任何元素,它會將其運算元與空陣列進行比對。也就是說,它永遠不會產生 undefinednull。例如

const [x, y, ...z] = ['a']; // x='a'; y=undefined; z=[]

剩餘運算子的運算元不一定要是變數,您也可以使用模式

const [x, ...[y, z]] = ['a', 'b', 'c'];
    // x = 'a'; y = 'b'; z = 'c'

剩餘運算子會觸發下列解構

[y, z] = ['b', 'c']

10.8 您可以指定給不只是變數

如果您透過解構指定,每個指定目標都可以是正常指定中左側允許的所有內容。

例如,屬性的參考 (obj.prop)

const obj = {};
({ foo: obj.prop } = { foo: 123 });
console.log(obj); // {prop:123}

或陣列元素的參考 (arr[0])

const arr = [];
({ bar: arr[0] } = { bar: true });
console.log(arr); // [true]

您也可以透過剩餘運算子 (...) 指定給物件屬性和陣列元素。

const obj = {};
[first, ...obj.prop] = ['a', 'b', 'c'];
    // first = 'a'; obj.prop = ['b', 'c']

如果您透過解構宣告變數或定義參數,則您必須使用簡單識別碼,您不能參考物件屬性和陣列元素。

10.9 解構的陷阱

使用解構時有兩件事要注意

後續兩個區塊包含詳細資訊。

10.9.1 陳述句不要以大括號開頭

由於程式區塊以大括號開頭,陳述句不能以大括號開頭。在指派中使用物件解構時,這很不幸

{ a, b } = someObject; // SyntaxError

解決方法是將完整表達式放在括號中

({ a, b } = someObject); // OK

下列語法無法運作

({ a, b }) = someObject; // SyntaxError

使用 letvarconst 時,大括號永遠不會造成問題

const { a, b } = someObject; // OK

10.10 解構範例

讓我們從幾個較小的範例開始。

for-of 迴圈支援解構

const map = new Map().set(false, 'no').set(true, 'yes');
for (const [key, value] of map) {
  console.log(key + ' is ' + value);
}

你可以使用解構來交換值。這是引擎可以最佳化的項目,因此不會建立陣列。

[a, b] = [b, a];

你可以使用解構來分割陣列

const [first, ...rest] = ['a', 'b', 'c'];
    // first = 'a'; rest = ['b', 'c']

10.10.1 解構傳回陣列

一些內建 JavaScript 作業會傳回陣列。解構有助於處理它們

const [all, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

如果你只對群組感興趣(而不是完整比對,all),你可以使用省略號來略過索引 0 的陣列元素

const [, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec('2999-12-31');

如果正規表示式不比對,exec() 會傳回 null。很不幸地,你無法透過預設值來處理 null,這就是為什麼你必須在此情況下使用或運算子 (||)

const [, year, month, day] =
    /^(\d\d\d\d)-(\d\d)-(\d\d)$/
    .exec(someStr) || [];

Array.prototype.split() 會傳回陣列。因此,如果你對元素感興趣,而不是陣列,解構會很有用

const cells = 'Jane\tDoe\tCTO'
const [firstName, lastName, title] = cells.split('\t');
console.log(firstName, lastName, title);

10.10.2 解構傳回物件

解構對於從函式或方法傳回的物件中萃取資料也很有用。例如,迭代器方法 next() 會傳回具有兩個屬性的物件,donevalue。下列程式碼透過迭代器 iter 記錄陣列 arr 的所有元素。解構用於 A 行。

const arr = ['a', 'b'];
const iter = arr[Symbol.iterator]();
while (true) {
    const {done,value} = iter.next(); // (A)
    if (done) break;
    console.log(value);
}

10.10.3 陣列解構可迭代值

陣列解構適用於任何可迭代值。這偶爾很有用

const [x,y] = new Set().add('a').add('b');
    // x = 'a'; y = 'b'

const [a,b] = 'foo';
    // a = 'f'; b = 'o'

10.10.4 多個傳回值

要了解多重回傳值的用途,我們實作一個函式 `findElement(a, p)`,用來在陣列 `a` 中尋找第一個函式 `p` 回傳 `true` 的元素。問題是:`findElement()` 應該回傳什麼?有時候我們感興趣的是元素本身,有時候是其索引,有時候是兩者。以下實作回傳兩者。

function findElement(array, predicate) {
    for (const [index, element] of array.entries()) { // (A)
        if (predicate(element, index, array)) {
            // We found an element:
            return { element, index };
                // Same as (property value shorthands):
                // { element: element, index: index }
        }
    }
    // We couldn’t find anything; return failure values:
    return { element: undefined, index: -1 };
}

此函式透過陣列方法 `entries()` 遍歷 `array` 的所有元素,此方法會回傳一個可遍歷的 `[index,element]` 成對資料(第 A 行)。成對資料的部分透過解構存取。

我們來使用 `findElement()`

const arr = [7, 8, 6];
const {element, index} = findElement(arr, x => x % 2 === 0);
    // element = 8, index = 1

幾個 ECMAScript 6 的功能讓我們可以撰寫更簡潔的程式碼:回呼函式是一個箭頭函式;回傳值透過一個物件模式解構,並使用屬性值簡寫。

由於 `index` 和 `element` 也指屬性金鑰,我們提到它們的順序並不重要。我們可以交換它們,不會有任何改變

const {index, element} = findElement(···);

我們已經成功處理需要索引和元素的情況。如果我們只對其中一個感興趣呢?事實證明,由於 ECMAScript 6,我們的實作也可以處理這個情況。而且與具有單一回傳值的函式相比,語法開銷很小。

const a = [7, 8, 6];

const {element} = findElement(a, x => x % 2 === 0);
    // element = 8

const {index} = findElement(a, x => x % 2 === 0);
    // index = 1

每次,我們只擷取我們需要的單一屬性的值。

10.11 解構演算法

本節從不同的角度來看解構:作為一個遞迴模式比對演算法。

最後,我將使用演算法來解釋以下兩個函式宣告之間的差異。

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

10.11.1 演算法

解構賦值看起來像這樣

«pattern» = «value»

我們要使用 `pattern` 從 `value` 中擷取資料。我現在將描述一個用於這樣做的演算法,在函式程式設計中稱為 *模式比對*(簡稱:*比對*)。演算法指定運算子 `←`(「比對」)用於解構賦值,它會比對 `pattern` 和 `value`,並在這樣做的同時將其賦值給變數

«pattern»  «value»

演算法透過遞迴規則指定,這些規則會拆解 `←` 運算子的兩個運算元。宣告式表示法可能需要一些時間才能習慣,但它使演算法的指定更簡潔。每個規則都有兩個部分

我們來看一個範例

在規則 (2c) 中,標頭表示如果存在至少一個屬性和零個或更多剩餘屬性的物件樣式,則執行此規則。該樣式與值 obj 相符。此規則的效果是執行持續運作,其中屬性值樣式與 obj.key 相符,而剩餘屬性與 obj 相符。

在規則 (2e) 中,標頭表示如果空物件樣式 {} 與值 obj 相符,則執行此規則。然後就沒有任何事要做。

每當呼叫演算法時,都會從上到下檢查規則,並且只執行第一個適用的規則。

我只顯示解構賦值的演算法。解構變數宣告和解構參數定義的工作方式類似。

我也不會涵蓋進階功能(計算屬性金鑰;屬性值速記;物件屬性和陣列元素作為賦值目標)。只會涵蓋基礎知識。

10.11.1.1 樣式

樣式為

以下各節描述這三種情況之一。

以下三節說明如何處理這三種情況。每個部分包含一個或多個編號規則。

10.11.1.2 變數
10.11.1.3 物件樣式
10.11.1.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);
}

10.11.2 套用演算法

在 ECMAScript 6 中,如果呼叫者使用物件文字,而被呼叫者使用解構,你可以模擬命名參數。這個模擬在 參數處理章節 中有詳細說明。以下程式碼顯示一個範例:函式 move1() 有兩個命名參數,xy

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

A 行中有三個預設值

但是,為什麼你要在先前的程式碼片段中定義參數?為什麼不如下列方式定義,這也是完全合法的 ES6 程式碼?

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

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

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

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

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

參數 ab 的設定方式與以下解構類似。

[a=0, b=0]  [1, 2]
10.11.2.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,這不是我們想要的。

10.11.2.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
10.11.2.4 結論

這些範例說明預設值是樣式部分(物件屬性或陣列元素)的一個功能。如果某個部分沒有相符或與 undefined 相符,則會使用預設值。也就是說,樣式會與預設值相符。

下一篇:11. 參數處理