給急躁的程式設計師的 JavaScript(ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

37 解構



37.1 初次體驗解構

使用一般指定時,您一次只會萃取一塊資料,例如

const arr = ['a', 'b', 'c'];
const x = arr[0]; // extract
const y = arr[1]; // extract

使用解構時,您可以透過接收資料的位置中的模式,同時萃取多塊資料。上一個程式碼中 = 的左手邊就是其中一個位置。在以下程式碼中,A 行中的方括弧是一個解構模式

const arr = ['a', 'b', 'c'];
const [x, y] = arr; // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');

此程式碼與上一個程式碼執行相同的工作。

請注意,模式「小於」資料:我們只萃取我們需要的部分。

37.2 建構與萃取

為了了解什麼是解構,請考慮 JavaScript 有兩種相反的運算

建構資料如下所示

// Constructing: one property at a time
const jane1 = {};
jane1.first = 'Jane';
jane1.last = 'Doe';

// Constructing: multiple properties
const jane2 = {
  first: 'Jane',
  last: 'Doe',
};

assert.deepEqual(jane1, jane2);

萃取資料如下所示

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

// Extracting: one property at a time
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');

// Extracting: multiple properties (NEW!)
const {first: f2, last: l2} = jane; // (A)
assert.equal(f2, 'Jane');
assert.equal(l2, 'Doe');

A 行中的運算很新:我們宣告兩個變數 f2l2,並透過解構(多值萃取)初始化它們。

A 行的以下部分是一個解構模式

{first: f2, last: l2}

解構模式在語法上類似於用於多值建構的文字。但它們出現在接收資料的地方(例如,指定左手邊),而不是建立資料的地方(例如,指定右手邊)。

37.3 我們可以在哪裡解構?

解構模式可以用於「資料接收位置」,例如

請注意,變數宣告包括 for-of 迴圈中的 constlet 宣告

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

接下來的兩個區段中,我們將深入探討兩種解構:物件解構和陣列解構。

37.4 物件解構

物件解構讓您可以透過看起來像物件文字的模式,批次萃取屬性的值

const address = {
  street: 'Evergreen Terrace',
  number: '742',
  city: 'Springfield',
  state: 'NT',
  zip: '49007',
};

const { street: s, city: c } = address;
assert.equal(s, 'Evergreen Terrace');
assert.equal(c, 'Springfield');

您可以將模式視為一個透明的紙張,您將它放在資料上:模式鍵 'street' 在資料中有一個匹配。因此,資料值 'Evergreen Terrace' 指定給模式變數 s

您也可以對基本值進行物件解構

const {length: len} = 'abc';
assert.equal(len, 3);

您也可以對陣列進行物件解構

const {0:x, 2:y} = ['a', 'b', 'c'];
assert.equal(x, 'a');
assert.equal(y, 'c');

為什麼這樣做有效?陣列索引也是屬性

37.4.1 屬性值速記

物件文字支援屬性值速記,物件模式也支援

const { street, city } = address;
assert.equal(street, 'Evergreen Terrace');
assert.equal(city, 'Springfield');

  練習:物件解構

exercises/destructuring/object_destructuring_exrc.mjs

37.4.2 剩餘屬性

在物件文字中,你可以有展開屬性。在物件模式中,你可以有剩餘屬性(必須放在最後)

const obj = { a: 1, b: 2, c: 3 };
const { a: propValue, ...remaining } = obj; // (A)

assert.equal(propValue, 1);
assert.deepEqual(remaining, {b:2, c:3});

剩餘屬性變數,例如 remaining(A 行),會指定一個物件,其中包含所有資料屬性,其鍵未在模式中提及。

remaining 也可以視為從 obj 非破壞性移除屬性 a 的結果。

37.4.3 語法陷阱:透過物件解構指定

如果我們在指定中進行物件解構,我們會遇到由 語法歧義 造成的陷阱 - 你不能以大括號開始陳述式,因為這樣 JavaScript 會認為你正在開始一個區塊

let prop;
assert.throws(
  () => eval("{prop} = { prop: 'hello' };"),
  {
    name: 'SyntaxError',
    message: "Unexpected token '='",
  });

  為什麼是 eval()

eval() 會延後解析(因此也會延後 SyntaxError),直到執行 assert.throws() 的回呼函式。如果我們沒有使用它,我們會在解析此程式碼時收到錯誤,而 assert.throws() 甚至不會執行。

解決方法是將整個指定放在括號中

let prop;
({prop} = { prop: 'hello' });
assert.equal(prop, 'hello');

37.5 陣列解構

陣列解構 讓你透過看起來像陣列文字的模式,批次萃取陣列元素的值

const [x, y] = ['a', 'b'];
assert.equal(x, 'a');
assert.equal(y, 'b');

你可以透過在陣列模式中提到空洞來略過元素

const [, x, y] = ['a', 'b', 'c']; // (A)
assert.equal(x, 'b');
assert.equal(y, 'c');

A 行中陣列模式的第一個元素是一個空洞,這就是為什麼會忽略索引為 0 的陣列元素。

37.5.1 陣列解構適用於任何可迭代物件

陣列解構可以套用在任何可迭代的值,不只陣列

// Sets are iterable
const mySet = new Set().add('a').add('b').add('c');
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');

// Strings are iterable
const [a, b] = 'xyz';
assert.equal(a, 'x');
assert.equal(b, 'y');

37.5.2 剩餘元素

在陣列文字中,你可以有展開元素。在陣列模式中,你可以有剩餘元素(必須放在最後)

const [x, y, ...remaining] = ['a', 'b', 'c', 'd']; // (A)

assert.equal(x, 'a');
assert.equal(y, 'b');
assert.deepEqual(remaining, ['c', 'd']);

剩餘元素變數,例如 remaining(A 行),會指定一個陣列,其中包含解構值中尚未提及的所有元素。

37.6 解構範例

37.6.1 陣列解構:交換變數值

你可以使用陣列解構來交換兩個變數的值,而不需要暫時變數

let x = 'a';
let y = 'b';

[x,y] = [y,x]; // swap

assert.equal(x, 'b');
assert.equal(y, 'a');

37.6.2 陣列解構:傳回陣列的運算

當運算傳回陣列時,陣列解構會很有用,例如正規表示法方法 .exec()

// Skip the element at index 0 (the whole match):
const [, year, month, day] =
  /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/
  .exec('2999-12-31');

assert.equal(year, '2999');
assert.equal(month, '12');
assert.equal(day, '31');

37.6.3 物件解構:多個傳回值

如果函式傳回多個值,解構會非常有用,這些值可以封裝為陣列或物件。

考慮一個在陣列中尋找元素的函式 findElement()

findElement(array, (value, index) => «boolean expression»)

它的第二個參數是一個函式,它接收元素的值和索引,並傳回一個布林值,表示這是否是呼叫者正在尋找的元素。

我們現在面臨一個兩難:findElement() 應該傳回它找到的元素的值還是索引?一個解決方案是建立兩個獨立的函式,但這將導致重複的程式碼,因為這兩個函式會非常相似。

以下實作透過傳回包含找到的元素索引和值的物件來避免重複

function findElement(arr, predicate) {
  for (let index=0; index < arr.length; index++) {
    const value = arr[index];
    if (predicate(value)) {
      // We found something:
      return { value, index };
    }
  }
  // We didn’t find anything:
  return { value: undefined, index: -1 };
}

解構有助於我們處理 findElement() 的結果

const arr = [7, 8, 6];

const {value, index} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);
assert.equal(index, 1);

由於我們處理的是屬性金鑰,因此我們提到 valueindex 的順序並不重要

const {index, value} = findElement(arr, x => x % 2 === 0);

關鍵在於,如果我們只對兩個結果之一感興趣,解構也能很好地為我們服務

const arr = [7, 8, 6];

const {value} = findElement(arr, x => x % 2 === 0);
assert.equal(value, 8);

const {index} = findElement(arr, x => x % 2 === 0);
assert.equal(index, 1);

所有這些便利性結合在一起,使得這種處理多個傳回值的方式非常靈活。

37.7 如果模式的一部分與任何東西都不相符,會發生什麼事?

如果模式的一部分沒有相符的項目,會發生什麼事?這與使用非批次運算子時發生的事情相同:你會得到 undefined

37.7.1 物件解構和遺失的屬性

如果物件模式中的屬性在右側沒有相符的項目,你會得到 undefined

const {prop: p} = {};
assert.equal(p, undefined);

37.7.2 陣列解構和遺失的元素

如果陣列模式中的元素在右側沒有相符的項目,你會得到 undefined

const [x] = [];
assert.equal(x, undefined);

37.8 哪些值無法解構?

37.8.1 你無法對 undefinednull 進行物件解構

只有當要解構的值是 undefinednull 時,物件解構才會失敗。也就是說,只要透過點運算子存取屬性也會失敗時,它就會失敗。

> const {prop} = undefined
TypeError: Cannot destructure property 'prop' of 'undefined'
as it is undefined.

> const {prop} = null
TypeError: Cannot destructure property 'prop' of 'null'
as it is null.

37.8.2 你無法對不可迭代的值進行陣列解構

陣列解構要求解構的值是可迭代的。因此,你無法對 undefinednull 進行陣列解構。但是你也不能對不可迭代的物件進行陣列解構

> const [x] = {}
TypeError: {} is not iterable

  測驗:基礎

請參閱 測驗應用程式

37.9 (進階)

其餘章節都是進階的。

37.10 預設值

通常,如果模式沒有匹配,對應的變數會設為 undefined

const {prop: p} = {};
assert.equal(p, undefined);

如果你想要使用不同的值,你需要指定一個 預設值(透過 =

const {prop: p = 123} = {}; // (A)
assert.equal(p, 123);

在 A 行,我們指定 p 的預設值為 123。會使用該預設值,因為我們解構的資料沒有名為 prop 的屬性。

37.10.1 陣列解構中的預設值

這裡,我們有兩個預設值,指定給變數 xy,因為對應的元素不存在於解構的陣列中。

const [x=1, y=2] = [];

assert.equal(x, 1);
assert.equal(y, 2);

陣列模式第一個元素的預設值為 1;第二個元素的預設值為 2

37.10.2 物件解構中的預設值

你也可以為物件解構指定預設值

const {first: f='', last: l=''} = {};
assert.equal(f, '');
assert.equal(l, '');

解構的物件中既沒有屬性鍵 first,也沒有屬性鍵 last。因此,會使用預設值。

使用屬性值簡寫,這段程式碼會變得更簡單

const {first='', last=''} = {};
assert.equal(first, '');
assert.equal(last, '');

37.11 參數定義類似於解構

考慮我們在本章學到的內容,參數定義與陣列模式有很多共同點(剩餘元素、預設值等)。事實上,以下兩個函式宣告是等效的

function f1(«pattern1», «pattern2») {
  // ···
}

function f2(...args) {
  const [«pattern1», «pattern2»] = args;
  // ···
}

37.12 巢狀解構

到目前為止,我們只在解構模式中使用變數作為 指定目標(資料接收器)。但你也可以使用模式作為指定目標,這使你能夠將模式巢狀到任意深度

const arr = [
  { first: 'Jane', last: 'Bond' },
  { first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr; // (A)
assert.equal(first, 'Lars');

在 A 行的陣列模式中,在索引 1 有巢狀的物件模式。

巢狀模式可能很難理解,因此最好適度使用。

  測驗:進階

請參閱 測驗應用程式