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

冒号显示“什么:去哪里”。在上面的示例中,属性 width 转到 w,属性 height 转到 h,而 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 来填充它)支持,但在现代浏览器中可以使用。

它看起来像这样

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
  • 如果有多个收入最高的人员,则返回其中任何一个。

P.S. 使用 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…)