2024 年 1 月 27 日

解構賦值

JavaScript 中使用最多的兩個資料結構是 ObjectArray

  • 物件允許我們建立一個單一實體,其中儲存資料項目,並以鍵值來存取。
  • 陣列允許我們將資料項目收集到一個順序清單中。

然而,當我們將這些資料傳遞給函式時,我們可能不需要全部。函式可能只需要某些元素或屬性。

解構賦值 是一種特殊的語法,允許我們將陣列或物件「解開」成一堆變數,因為有時這樣會更方便。

解構也適用於具有許多參數、預設值等複雜函式。我們很快就會看到。

陣列解構

以下是一個範例,說明如何將陣列解構為變數

// we have an array with a name and surname
let arr = ["John", "Smith"]

// destructuring assignment
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;

alert(firstName); // John
alert(surname);  // Smith

現在我們可以使用變數,而不是陣列成員。

當與 split 或其他回傳陣列的方法結合使用時,看起來很棒

let [firstName, surname] = "John Smith".split(' ');
alert(firstName); // John
alert(surname);  // Smith

如你所見,語法很簡單。不過有幾個特殊的細節。讓我們看更多範例來更了解它。

「解構」並不表示「破壞」。

它稱為「解構賦值」,因為它透過將項目複製到變數中來「解構」。然而,陣列本身並未修改。

這只是寫作的一種簡短方式

// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
使用逗號忽略元素

陣列中不需要的元素也可以透過額外的逗號捨棄

// second element is not needed
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];

alert( title ); // Consul

在上面的程式碼中,陣列的第二個元素被跳過,第三個元素被指定給 title,而陣列項目的其餘部分也被跳過(因為它們沒有變數)。

與右側的任何可迭代物件一起使用

…實際上,我們可以使用它與任何可迭代物件一起使用,而不仅仅是陣列

let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);

這樣做是因為在內部,解構賦值會透過迭代右側的值來運作。這是一種語法糖,用於呼叫右側 =for..of 並指定值。

指定給左側的任何內容

我們可以在左側使用任何「可指定」的內容。

例如,物件屬性

let user = {};
[user.name, user.surname] = "John Smith".split(' ');

alert(user.name); // John
alert(user.surname); // Smith
使用 .entries() 迴圈

在上一章中,我們看到了 Object.entries(obj) 方法。

我們可以使用它與解構一起迴圈物件的鍵和值

let user = {
  name: "John",
  age: 30
};

// loop over the keys-and-values
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, then age:30
}

Map 的類似程式碼較為簡單,因為它是可迭代的

let user = new Map();
user.set("name", "John");
user.set("age", "30");

// Map iterates as [key, value] pairs, very convenient for destructuring
for (let [key, value] of user) {
  alert(`${key}:${value}`); // name:John, then age:30
}
變數交換技巧

有一個眾所周知的技巧,可以使用解構賦值來交換兩個變數的值

let guest = "Jane";
let admin = "Pete";

// Let's swap the values: make guest=Pete, admin=Jane
[guest, admin] = [admin, guest];

alert(`${guest} ${admin}`); // Pete Jane (successfully swapped!)

在這裡,我們建立一個由兩個變數組成的暫時陣列,並立即以交換的順序解構它。

我們可以透過這種方式交換兩個以上的變數。

其餘的「…」

通常,如果陣列比左側的清單長,則會省略「額外」的項目。

例如,這裡只取兩個項目,而其餘的項目則被忽略

let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];

alert(name1); // Julius
alert(name2); // Caesar
// Further items aren't assigned anywhere

如果我們還想收集所有後續項目,我們可以新增另一個參數,使用三個點 "..." 來取得「其餘」的項目

let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];

// rest is an array of items, starting from the 3rd one
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2

rest 的值是陣列中剩餘陣列元素的陣列。

我們可以在 rest 的位置使用任何其他變數名稱,只要確保它之前有三個點,並且在解構賦值中最後出現即可。

let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// now titles = ["Consul", "of the Roman Republic"]

預設值

如果陣列比左側的變數清單短,則不會有錯誤。不存在的值會被視為未定義

let [firstName, surname] = [];

alert(firstName); // undefined
alert(surname); // undefined

如果我們想要一個「預設」值來取代遺失的值,我們可以使用 = 提供它

// default values
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name);    // Julius (from array)
alert(surname); // Anonymous (default used)

預設值可以是更複雜的表達式,甚至是函式呼叫。它們只會在未提供值時評估。

例如,這裡我們對兩個預設值使用 prompt 函式

// runs only prompt for surname
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];

alert(name);    // Julius (from array)
alert(surname); // whatever prompt gets

請注意:prompt 僅會對遺失的值(surname)執行。

物件解構

解構賦值也適用於物件。

基本語法為

let {var1, var2} = {var1:…, var2:…}

我們應該在右側有一個現有的物件,我們想要將其拆分為變數。左側包含一個類似物件的「模式」,用於對應的屬性。在最簡單的情況下,這是一個在 {...} 中的變數名稱清單。

例如

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

let {title, width, height} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200

屬性 options.titleoptions.widthoptions.height 分別指定給對應的變數。

順序無關緊要。這也行得通

// changed the order in let {...}
let {height, width, title} = { title: "Menu", height: 200, width: 100 }

左側的模式可能會更複雜,並指定屬性和變數之間的對應關係。

如果我們想要將一個屬性指定給另一個名稱的變數,例如,讓 options.width 進入名為 w 的變數,那麼我們可以使用冒號來設定變數名稱

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;

// width -> w
// height -> h
// title -> title

alert(title);  // Menu
alert(w);      // 100
alert(h);      // 200

冒號表示「什麼 : 到哪裡」。在上面的範例中,屬性 widthw,屬性 heighth,而 title 指定給同一個名稱。

對於可能遺失的屬性,我們可以使用 "=" 設定預設值,如下所示

let options = {
  title: "Menu"
};

let {width = 100, height = 200, title} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200

就像陣列或函式參數一樣,預設值可以是任何表達式,甚至是函式呼叫。如果未提供值,它們將會被評估。

在下面的程式碼中,prompt 詢問 width,但沒有詢問 title

let options = {
  title: "Menu"
};

let {width = prompt("width?"), title = prompt("title?")} = options;

alert(title);  // Menu
alert(width);  // (whatever the result of prompt is)

我們也可以同時結合冒號和等號

let options = {
  title: "Menu"
};

let {width: w = 100, height: h = 200, title} = options;

alert(title);  // Menu
alert(w);      // 100
alert(h);      // 200

如果我們有一個具有許多屬性的複雜物件,我們可以只擷取我們需要的部分

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

// only extract title as a variable
let { title } = options;

alert(title); // Menu

剩餘模式「…」

如果物件的屬性比我們的變數多怎麼辦?我們可以取一些,然後將「剩餘」指定到某個地方嗎?

我們可以使用剩餘模式,就像我們對陣列所做的那樣。它不受一些較舊的瀏覽器支援(IE,使用 Babel 來進行 polyfill),但在現代瀏覽器中可以使用。

它看起來像這樣

let options = {
  title: "Menu",
  height: 200,
  width: 100
};

// title = property named title
// rest = object with the rest of properties
let {title, ...rest} = options;

// now title="Menu", rest={height: 200, width: 100}
alert(rest.height);  // 200
alert(rest.width);   // 100
如果沒有 let,那就完了

在上面的範例中,變數直接在賦值中宣告:let {…} = {…}。當然,我們也可以使用現有的變數,而不用 let。但有一個陷阱。

這不會起作用

let title, width, height;

// error in this line
{title, width, height} = {title: "Menu", width: 200, height: 100};

問題在於 JavaScript 將主程式碼流程中的 {...}(不在另一個表達式中)視為程式碼區塊。這樣的程式碼區塊可用於將陳述式分組,如下所示

{
  // a code block
  let message = "Hello";
  // ...
  alert( message );
}

所以這裡 JavaScript 假設我們有一個程式碼區塊,這就是為什麼會發生錯誤。我們想要的是解構。

為了向 JavaScript 顯示它不是程式碼區塊,我們可以用括號 (...) 包住表達式

let title, width, height;

// okay now
({title, width, height} = {title: "Menu", width: 200, height: 100});

alert( title ); // Menu

巢狀解構

如果物件或陣列包含其他巢狀物件和陣列,我們可以使用更複雜的左側模式來擷取更深入的部分。

在下方程式碼中,optionssize 屬性中具有另一個物件,在 items 屬性中具有陣列。指派左側的模式具有相同的結構,以從中擷取值

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true
};

// destructuring assignment split in multiple lines for clarity
let {
  size: { // put size here
    width,
    height
  },
  items: [item1, item2], // assign items here
  title = "Menu" // not present in the object (default value is used)
} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
alert(item1);  // Cake
alert(item2);  // Donut

options 物件的所有屬性,除了左側沒有的 extra,都指派給對應的變數

最後,我們從預設值取得 widthheightitem1item2title

請注意,sizeitems 沒有變數,因為我們取用它們的內容。

智慧型函式參數

有時函式有許多參數,其中大部分都是選用的。這在使用者介面中特別常見。想像一個建立選單的函式。它可能具有寬度、高度、標題、項目清單等。

以下是撰寫此類函式的錯誤方式

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
  // ...
}

在實際情況中,問題是如何記住參數的順序。通常,IDE 會嘗試協助我們,特別是如果程式碼有良好的文件說明,但還是…另一個問題是如何在大部分參數預設為確定時呼叫函式。

像這樣嗎?

// undefined where default values are fine
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])

很醜陋。當我們處理更多參數時,會變得難以閱讀。

解構來救援!

我們可以將參數傳遞為物件,而函式會立即將它們解構為變數

// we pass object to function
let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

// ...and it immediately expands it to variables
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
  // title, items – taken from options,
  // width, height – defaults used
  alert( `${title} ${width} ${height}` ); // My Menu 200 100
  alert( items ); // Item1, Item2
}

showMenu(options);

我們也可以使用更複雜的解構,包含巢狀物件和冒號對應

let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

function showMenu({
  title = "Untitled",
  width: w = 100,  // width goes to w
  height: h = 200, // height goes to h
  items: [item1, item2] // items first element goes to item1, second to item2
}) {
  alert( `${title} ${w} ${h}` ); // My Menu 100 200
  alert( item1 ); // Item1
  alert( item2 ); // Item2
}

showMenu(options);

完整的語法與解構指派相同

function({
  incomingProperty: varName = defaultValue
  ...
})

然後,對於參數物件,將會有變數 varName 對應屬性 incomingProperty,預設為 defaultValue

請注意,此類解構假設 showMenu() 確實有參數。如果我們要使用所有預設值,則應指定一個空物件

showMenu({}); // ok, all values are default

showMenu(); // this would give an error

我們可以透過將 {} 設定為參數整個物件的預設值來修正此問題

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert( `${title} ${width} ${height}` );
}

showMenu(); // Menu 100 200

在上方程式碼中,整個參數物件預設為 {},因此總是有東西可以解構。

摘要

  • 解構賦值允許立即將物件或陣列對應到多個變數。

  • 完整的物件語法

    let {prop : varName = defaultValue, ...rest} = object

    這表示屬性 prop 應放入變數 varName 中,如果沒有此屬性,則應使用 default 值。

    沒有對應的物件屬性會複製到 rest 物件中。

  • 完整的陣列語法

    let [item1 = defaultValue, item2, ...rest] = array

    第一個項目會放入 item1 中;第二個會放入 item2 中,而所有其他項目會組成陣列 rest

  • 可以從巢狀陣列/物件中萃取資料,為此,左側必須與右側具有相同的結構。

任務

重要性:5

我們有一個物件

let user = {
  name: "John",
  years: 30
};

撰寫解構賦值,讀取

  • name 屬性到變數 name 中。
  • years 屬性到變數 age 中。
  • isAdmin 屬性到變數 isAdmin 中(如果沒有此屬性,則為 false)

以下是賦值後的範例值

let user = { name: "John", years: 30 };

// your code to the left side:
// ... = user

alert( name ); // John
alert( age ); // 30
alert( isAdmin ); // false
let user = {
  name: "John",
  years: 30
};

let {name, years: age, isAdmin = false} = user;

alert( name ); // John
alert( age ); // 30
alert( isAdmin ); // false
重要性:5

有一個 salaries 物件

let salaries = {
  "John": 100,
  "Pete": 300,
  "Mary": 250
};

建立函式 topSalary(salaries),傳回薪資最高的人員姓名。

  • 如果 salaries 為空,則應傳回 null
  • 如果有多位薪資最高的人員,則傳回其中任何一位。

補充說明:使用 Object.entries 和解構來反覆運算鍵值對。

開啟包含測試的沙盒。

function topSalary(salaries) {

  let maxSalary = 0;
  let maxName = null;

  for(const [name, salary] of Object.entries(salaries)) {
    if (maxSalary < salary) {
      maxSalary = salary;
      maxName = name;
    }
  }

  return maxName;
}

在沙盒中開啟包含測試的解答。

教學課程地圖

留言

留言前請先閱讀...
  • 如果您有改善建議,請 提交 GitHub 問題 或提出プルリクエスト,而非留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入少數幾個字元的程式碼,請使用 <code> 標籤;若要插入數行程式碼,請將它們包覆在 <pre> 標籤中;若要插入超過 10 行程式碼,請使用沙盒 (plnkrjsbincodepen…)