数组的解构赋值

基本用法

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

es5一次声明多个变量

  1. var a = 1,
  2. b = 2,
  3. c = 3,
  4. ...;

es6一次声明多个变量

  1. let [a,b,c] = [1,2,3];
  2. //a = 1
  3. //b = 2
  4. //c = 3

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

  1. let [foo, [[bar], baz]] = [1, [[2], 3]];
  2. foo // 1
  3. bar // 2
  4. baz // 3
  5. let [ , , third] = ["foo", "bar", "baz"];
  6. third // "baz"
  7. let [x, , y] = [1, 2, 3];
  8. x // 1
  9. y // 3
  10. let [head, ...tail] = [1, 2, 3, 4];
  11. head // 1
  12. tail // [2, 3, 4]
  13. let [x, y, ...z] = ['a'];
  14. x // "a"
  15. y // undefined
  16. z // []

如果解构不成功,变量的值就等于undefined

  1. let [foo] = [];
  2. let [bar, foo] = [1];
  3. //foo 都是undefined

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

  1. let [x, y] = [1, 2, 3];
  2. x // 1
  3. y // 2
  4. let [a, [b], d] = [1, [2, 3], 4];
  5. a // 1
  6. b // 2
  7. d // 4
  8. //上面两个例子,都属于不完全解构,但是可以成功。

如果等号的右边不是数组,那么将会报错。

  1. // 报错
  2. let [foo] = 1;
  3. let [foo] = false;
  4. let [foo] = NaN;
  5. let [foo] = undefined;
  6. let [foo] = null;
  7. let [foo] = {};

默认值

解构赋值允许指定默认值。

  1. let [foo = true] = [];
  2. foo // true
  3. let [x, y = 'b'] = ['a']; // x='a', y='b'
  4. let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

  1. let [x = 1] = [undefined];
  2. x // 1
  3. let [x = 1] = [null];
  4. x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

  1. function f() {
  2. console.log('aaa');
  3. }
  4. let [x = f()] = [1];
  5. //等价于
  6. let x;
  7. if ([1][0] === undefined) {
  8. x = f();
  9. } else {
  10. x = [1][0];
  11. }

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

  1. let [x = 1, y = x] = []; // x=1; y=1
  2. let [x = 1, y = x] = [2]; // x=2; y=2
  3. let [x = 1, y = x] = [1, 2]; // x=1; y=2
  4. let [x = y, y = 1] = []; // ReferenceError
  5. //上面最后一个表达式之所以会报错,是因为x用到默认值y时,y还没有声明

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

  1. let { foo, bar } = { foo: "aaa", bar: "bbb" };
  2. foo // "aaa"
  3. bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

  1. let { bar, foo } = { foo: "aaa", bar: "bbb" };
  2. foo // "aaa"
  3. bar // "bbb"
  4. let { baz } = { foo: "aaa", bar: "bbb" };
  5. baz // undefined

如果变量名与属性名不一致,必须写成下面这样。

  1. let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
  2. baz // "aaa"
  3. let obj = { first: 'hello', last: 'world' };
  4. let { first: f, last: l } = obj;
  5. f // 'hello'
  6. l // 'world'

实际上说明,对象的解构赋值是下面形式的简写

  1. let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

  1. let { foo: baz } = { foo: "aaa", bar: "bbb" };
  2. baz // "aaa"
  3. foo // error: foo is not defined

与数组一样,解构也可以用于嵌套结构的对象。

  1. let obj = {
  2. p: [
  3. 'Hello',
  4. { y: 'World' }
  5. ]
  6. };
  7. let { p: [x, { y }] } = obj;
  8. x // "Hello"
  9. y // "World"

对象的解构也可以指定默认值。

  1. var {x = 3} = {};
  2. x // 3
  3. var {x, y = 5} = {x: 1};
  4. x // 1
  5. y // 5
  6. var {x: y = 3} = {};
  7. y // 3
  8. var {x: y = 3} = {x: 5};
  9. y // 5
  10. var { message: msg = 'Something went wrong' } = {};
  11. msg // "Something went wrong"

默认值生效的条件是,对象的属性值严格等于undefined

  1. var {x = 3} = {x: undefined};
  2. x // 3
  3. var {x = 3} = {x: null};
  4. x // null

如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

  1. // 报错
  2. let {foo: {bar}} = {baz: 'baz'};
  3. //等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错,

由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

  1. let arr = [1, 2, 3];
  2. let {0 : first, [arr.length - 1] : last} = arr;
  3. first // 1
  4. last // 3

字符串的解构赋值

  1. const [a, b, c, d, e] = 'hello';
  2. a // "h"
  3. b // "e"
  4. c // "l"
  5. d // "l"
  6. e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

  1. let {length : len} = 'hello';
  2. len // 5

函数参数的解构赋值

  1. function add([a,b]){
  2. return a+b;
  3. }
  4. add([2,3])//5

函数参数的解构也可以使用默认值。

  1. function move({x = 0, y = 0} = {}) {
  2. return [x, y];
  3. }
  4. move({x: 3, y: 8}); // [3, 8]
  5. move({x: 3}); // [3, 0]
  6. move({}); // [0, 0]
  7. move(); // [0, 0]

注意,下面的写法会得到不一样的结果。

  1. function move({x, y} = { x: 0, y: 0 }) {
  2. return [x, y];
  3. }
  4. move({x: 3, y: 8}); // [3, 8]
  5. move({x: 3}); // [3, undefined]
  6. move({}); // [undefined, undefined]
  7. move(); // [0, 0]
  8. //

上面代码是为函数move的参数指定默认值,而不是为变量xy指定默认值,所以会得到与前一种写法不同的结果。

数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

  1. let {toString: s} = 123;
  2. s === Number.prototype.toString // true
  3. let {toString: s} = true;
  4. s === Boolean.prototype.toString // true

圆括号

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

不能使用圆括号的情况

以下三种解构赋值不得使用圆括号。

  1. 变量声明语句 ```javascript // 全部报错 let [(a)] = [1];

let {x: (c)} = {}; let ({x: c}) = {}; let {(x: c)} = {}; let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

  1. 2. 函数参数
  2. <br />函数参数也属于变量声明,因此不能带有圆括号。
  3. ```javascript
  4. // 报错
  5. function f([(z)]) { return z; }
  6. // 报错
  7. function f([z,(x)]) { return x; }
  1. 赋值语句的模式
    1. // 全部报错
    2. ({ p: a }) = { p: 42 };
    3. ([a]) = [5];
    4. //上面代码将整个模式放在圆括号之中,导致报错。
    5. // 报错
    6. [({ p: a }), { x: c }] = [{}, {}];

可以使用圆括号的情况

可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

  1. [(b)] = [3]; // 正确
  2. ({ p: (d) } = {}); // 正确
  3. [(parseInt.prop)] = [3]; // 正确

上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

用途

  1. 除了可以一次定义多个变量
  2. 还可以让函数返回多个值
  3. 可以方便地让函数的参数跟值对应起来
  4. 提取json数据
  5. 函数参数的默认值