32.解构

32.1 第一次尝试解构

通过普通赋值方式,你一次只能获取数据中的一个元素,例如,通过:

const arr = ['a', 'b', 'c'];
const x = arr[0]; // 只能获取第一个
const y = arr[1]; // 只能获取第二个

通过解构方式,你可以同时获取数据中的几个元素,通过在接收数据的位置进行模式匹配。在上方的代码的=左侧就是这样一个位置。在下方的代码中,行A中的方括号就是一个解构模式:

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

这段代码做了和之前的代码同样的事。

注意,这种方式比整个数据要“小巧”:我们只获取我们需要的数据元素。(译者注:冒号后面的话才是核心)

32.2 构造与提取

为了明白什么是解构,假想JavaScript有两种截然相反的操作:

  • 你可以 构造 复合数据,例如通过设置属性和对象文字的方式。
  • 你可以从复合数据中 提取 数据,例如通过获取属性的方式。

构造数据看起来像这样:

// 一次添加一个属性
const jane1 = {};
jane1.first = 'Jane';
jane1.last = 'Doe';
// 一次添加很多属性
const jane2 = {
  first: 'Jane',
  last: 'Doe',
};
assert.deepEqual(jane1, jane2);

提取数据看起来像这样:

const jane = {
  first: 'Jane',
  last: 'Doe',
};
// 一次获取一个属性
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');
// 一次获取多个属性(新方式!)
const {first: f2, last: l2} = jane; // (A)
assert.equal(f2, 'Jane');
assert.equal(l2, 'Doe');

行A中的操作方式是全新的:我们声明两个变量f2l2,并通过 解构 方式(多值提取)初始化它们。下方行A的部分是一个 解构 模式:

{first: f2, last: l2}

解构模式在语法上类似于用于多值构造的写法。但是它们出现在接收数据的地方(在赋值语句的左边),而不是创建数据的地方(在赋值语句的右边)。

32.3 我们在哪里可以使用解构?

解构模式可用于“数据接收位置”,例如:

  • 变量声明:
    const [a] = ['x'];
    assert.equal(a, 'x');
    let [b] = ['y'];
    assert.equal(b, 'y');
    
  • 赋值语句:
    let b;
    [b] = ['z'];
    assert.equal(b, 'z');
    
  • 参数定义:
    const f = ([x]) => x;
    assert.equal(f(['a']), 'a');
    

注意,变量的定义包括在for-of循环中的constlet声明:

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

在接下来的两部分中,我们将深入研究两种类型的解构:对象解构和数组解构。

32.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');

为什么这样可以?因为数组索引也是属性

32.4.1 属性值缩写

对象写法支持属性值缩写,对象解构模式也支持:

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

32.4.2 Rest属性

在对象写法中,你可以拥有扩展运算符。在对象模式中,可以使用rest属性(必须放在最后):

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});

一个rest参数,例如remaining(A行)被赋予一个对象,该对象具有模式中没有提到键的所有数据属性。

remaining也可以看作是从obj中非破坏性地删除属性a的结果。(译者注:不再需要a属性,但不想改变原对象obj,得到一个新的不包含a的数组remaining

32.4.3 语法陷阱:通过对象解构来赋值

如果我们在赋值过程中使用对象解构,我们就会面临语法歧义带来的陷阱——你不能用大括号来开始一个语句,因为JavaScript会认为你在开始一个代码块:

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

为什么eval()

eval()会延迟解析(因此也会延迟SyntaxError),直到执行assert.throw()的回调。如果我们不使用它,在解析这段代码时就会得到一个错误,assert.throw()甚至不会执行。

解决办法是把整个赋值语句放在括号里:

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

32.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的数组元素被忽略的原因。

32.5.1 数组解构可以用于任何可遍历的数据结构

数组解构可以用于任何可遍历的值,而不仅仅是数组:

// Sets数据结构是iterable的
const mySet = new Set().add('a').add('b').add('c');
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');
// 字符串数据结构是iterable的
const [a, b] = 'xyz';
assert.equal(a, 'x');
assert.equal(b, 'y');

32.5.2 Rest参数

在数组文字中,你可以使用扩展运算符。在数组模式中,你可以使用rest参数(必须放在最后):

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

rest元素变量,例如remaining(A行)被赋予一个数组,其中包含尚未提到的已解构值的所有元素。

32.6 解构的一些例子

32.6.1 数组解构:交换变量的值

你可以使用数组解构来交换两个变量的值,而不需要临时变量:

let x = 'a';
let y = 'b';
[x,y] = [y,x]; // swap
assert.equal(x, 'b');
assert.equal(y, 'a');

32.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');

32.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)) {
      // 有所发现的话
      return { value, index };
    }
  }
  // 什么都没找到的话
  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);

当我们使用属性的键名时,我们声明值和索引时的顺序并不重要:

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);

这些方便结合在一起,使得这种处理多个返回值的方法非常通用。

32.7 如果一个模式的一部分不匹配,会发生什么?

如果一个模式的一部分不匹配,会发生什么?如果使用非批处理操作符也会发生同样的事情:您将得到undefined的结果。

32.7.1 对象解构时缺少属性

如果对象模式中的属性在右侧没有匹配项,则得到undefined

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

32.7.2 数组解构时缺少元素

如果数组模式中的元素在右侧没有匹配项,则得到undefined

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

32.8 什么值无法被解构?

32.8.1 你不能对undefinednull使用对象解构

只有当要解构的值未定义(undefined)或为空(null)时,对象解构才会失败。也就是说,当通过点操作符访问属性时也会失败。

assert.throws(
  () => { const {prop} = undefined; },
  {
    name: 'TypeError',
    message: "Cannot destructure property `prop` of " +
      "'undefined' or 'null'.",
  }
);
assert.throws(
  () => { const {prop} = null; },
  {
    name: 'TypeError',
    message: "Cannot destructure property `prop` of " +
      "'undefined' or 'null'.",
  }
);

32.8.2 你不能对无法遍历的值进行数组解构

数组解构要求解构值是可遍历的。因此,不能对未定义(undefined)或者空(null)的数组进行解构,同样你也不能解构无法遍历的对象:

assert.throws(
  () => { const [x] = {}; },
  {
    name: 'TypeError',
    message: '{} is not iterable',
  }
);

测试:基本

请看 quiz app

32.9 (高级用法)

余下部分皆为高级用法。

32.10 默认值

一般情况下,如果一个模式没有匹配,则将对应的变量设置为undefined

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

如果你想要设定一个同样用途的不同的值,你需要指定一个初始值(通过=):

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

在行A中,我们指定p的默认值为123。该默认值会生效,因为要解构的的数据中没有名为prop的属性。

32.10.1 默认值在数组解构中的用法

在这里,我们有两个默认值被赋给变量xy,因为被解构的数组中不存在相应的元素。

const [x=1, y=2] = [];
assert.equal(x, 1);
assert.equal(y, 2);

给数组模式的第一个元素的默认值是1,给第二个元素的值是2

32.10.2 默认值对象解构中的用法

你也可以为对象解构指定默认值:

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

属性键first和属性键last都不存在于被解构的对象中。因此,默认值会生效。使用属性值缩写,代码会变得更简单:

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

32.11 参数定义和解构的相似性

考虑到我们在本章所学到的内容,参数定义与数组模式(rest元素、默认值等)有很多共同点。事实上,以下两个函数声明是等价的:

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

32.12 嵌套解构

到目前为止,我们只在解构模式中使用变量作为 赋值目标 (数据接收方)。但是你也可以使用模式作为赋值目标,这使你能够将模式嵌套到任意深度:

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

在A行的数组模式中,有一个数组嵌套,位于索引1。(译者注:即{first},实际上作者并没有注释A行在何处,是我后来添加上去的。或许作者是故意的呢?哈哈。)

嵌套模式不太容易理解,所以最好适当地使用。

测试:高级用法

请看quiz app