ES6躬行记(3)——解构

时间:2021-04-17 12:36:20

  解构(destructuring)是一种赋值语法,可从数组中提取元素或从对象中提取属性,将其值赋给对应的变量或另一个对象的属性。解构地目的是简化提取数据的过程,增强代码的可读性。有两种解构语法,分别是数组解构和对象解构,两者的区别在于解构赋值语句的左侧,前者是数组字面量,而后者是对象字面量。为了说明解构的优势,下面用一个例子来对比手动赋值和解构赋值。

var arr = [1, 2],
obj = { a: 3, b: 4 },
x, y, a, b;
x = arr[0];
y = arr[1];
a = obj.a;
b = obj.b; var [x, y] = [1, 2]; //数组解构
var { a, b } = { a: 3, b: 4 }; //对象解构

  从上面的代码中可看出,解构赋值只需2条语句,就能完成7条左右的语句才能实现的手动赋值。接下来会先介绍两种解构语法都包含的通用特性,然后分别讲解两者所独有的特性,再对它们的两个特性做对比,最后会分析一种应用解构的特殊场景:函数中的参数解构。

一、通用特性

  解构赋值的语句可以包含或忽略声明关键字(例如var、let等),如果包含,那么就需要初始化,否则会报语法错误,下面是两种不正确的写法。

var [x, y];
var { a, b };

  如果忽略声明关键字,那么在运行对象解构的时候,必须用圆括号包裹赋值表达式,而数组解构则并不强制,如下所示。

[x, y] = [1, 2];                     //无圆括号的数组解构
({ a, b } = { a: 3, b: 4 }); //有圆括号的对象解构

  之所以要用圆括号包裹,是因为表达式左侧的花括号会被解析成代码块而不是对象字面量。如果把代码块和等号运算符放在一行,那么就会报语法错误。

  当执行解构语句时,如果等号左侧的变量或对象属性没有从右侧找到对应的数组元素或对象属性,那么就会被赋为undefined,如下所示。

[c, d] = [1];
({ e, f } = { e: 3 });
console.log(d); //undefined
console.log(f); //undefined

二、数组解构

1)位置

  在数组解构时,解构会按顺序作用于数组的元素上,也就是说,变量或对象属性要取谁的值与它所在的位置有关。下面是一个交换变量位置的例子。

[x, y] = [1, 2];
console.log(x, y); //1 2
[y, x] = [1, 2];
console.log(x, y); //2 1

  位置交换后,变量被赋的值也会随之改变。如果将变量替换为对象属性,那么得到的结果也是相同的,如下所示。

var obj = { x, y };
[obj.x, obj.y] = [1, 2];
console.log(obj.x, obj.y); //1 2
[obj.y, obj.x] = [1, 2];
console.log(obj.x, obj.y); //2 1

  在经典的冒泡排序中,会交换两个值的位置,传统的做法是用一个临时变量做中转。而如果用解构的语法,那么就可以省略那个临时变量,并且写法更为简洁。两者的对比如下所示。

var x = 1,
y = 2;
[x, y] = [y, x]; //数组解构 var tmp = x; //传统做法
x = y;
y = tmp;

2)赋值

  数组解构还可以有选择性的赋值,只要在数组指定的位置上不提供元素,就能为其省去解构赋值。下面是一个实现的例子,只为数组的第3个元素提供变量名,在此之前只有两个用于占位的逗号。

[, , z] = [1, 2, 3];
console.log(z); //

  解构赋值表达式的右侧必须是可迭代对象,否则就会报错。下面的语句都是不正确的。

[x, y] = NaN;
[x, y] = undefined;
[x, y] = null;

  前面第2篇中曾用扩展运算符处理数组,但只在赋值表达式的右侧使用了扩展运算符。其实,还可以把扩展运算符放到表达式的左侧,如下所示。

[x, ...y] = [1, 2, 3];
console.log(x); //
console.log(y); //[2,3]

  在上面的代码中,可将...y称为剩余元素。右侧数组的第一个元素赋给了x变量,剩下的两个元素被收集起来赋给了y数组。注意,剩余元素必须是数组的最后一个元素,并且后面不能有逗号。

二、对象解构

1)赋值

  由于对象的属性没有按顺序排列,所以解构对象只会根据属性的名称是否相同来取值。下面代码的第一行是对象解构的简写形式,其效果相当于第二行语句。

({ a, b } = { b: 1, a: 2 });
({ a: a, b: b } = { b: 1, a: 2 });

  用上面的代码来描述对象解构的原理,步骤是:先找到同名属性(冒号左边的a或b),然后把各自的属性值对应起来,等号左侧的属性值(冒号右边的a或b)表示赋值目标,等号右侧的属性值(1或2)表示要提取的值。由此可知,匹配同名属性只是为了定位,真正被赋值的是处在属性值位置上的变量或另一个对象的属性。下面会用不同名称(即应用别名)的变量和属性来执行对象解构。

({ a: e, b: f } = { b: 5, a: 6 });
console.log(e, f); //6 5 var obj = { e, f };
({ a: obj.e, b: obj.f } = { b: 5, a: 6 });
console.log(obj.e, obj.f); //6 5

2)属性

  对象解构允许出现多个同名属性,如下代码所示,等号左侧的对象中虽然包含了两个a属性,但两个变量e和f都被成功赋了值。

({ a: e, a: f } = { b: 5, a: 6 });
console.log(e, f); //6 6

  ES6允许对象字面量的属性名用表达式定义(即属性名可计算),语法也很简单,只需将属性名用方括号包裹。对于这类属性,对象解构也能正确执行,如下所示。

var obj = { preName: "strick" },
attr = "Name";
({ ["pre" + attr]: value } = obj); //属性名是表达式
console.log(value); //"strick"

四、特性对比

  数组解构和对象解构都能包含一个可供赋值的默认值。如果是数组解构并且指定位置的元素不存在或其值不存在,那么就会使用默认值。判断元素的值是否存在,只要与undefined做全等(===)比较,当结果为真时,表示值不存在。下面的三个赋值表达式都包含了默认值,分别表示三种数组解构的情况。

[x, y=2] = [1];
console.log(y); //
[x, y=2] = [1, undefined];
console.log(y); //
[x, y=2] = [1, null];
console.log(y); //null

1)默认值

  对象解构使用默认值的判断依据是属性或属性值是否存在,属性值与前面的元素值一样,也要与undefined做全等比较。下面是三种对象解构的情况,解构条件与上面的数组解构类似。

({ a, b=2 } = { a: 1 });
console.log(b); //
({ a, b=2 } = { a: 1, b: undefined });
console.log(b); //
({ a, b=2 } = { a: 1, b: null });
console.log(b); //null

  上面代码使用了对象解构的简写形式,如果要为别名变量提供默认值,那么就要在别名变量右边加等号,如下所示。

({ a, b: digit=2 } = { a: 1 });
console.log(digit); //

2)嵌套解构

  数组解构和对象解构都支持嵌套解构,这是解构的一种复杂应用,可深入到嵌套的对象或数组中提取对应的数据,下面是两种语法的嵌套解构。

[x, [y], z] = [1, [2, 3], 4];
console.log(x, y, z); //1 2 4
({ a: { b: digit } } = { a: { b: 1 } });
console.log(digit); //

  在上面代码的第一条语句中,数组的第二个元素也是数组,即一个数组嵌套了另一个数组。y变量被赋予了内嵌数组的第一个元素,注意,没有把内嵌数组赋给y变量。在第三条语句中,对象的a属性值也是对象,即一个对象嵌套了另一个对象,digit变量被赋予了内嵌对象的b属性的值。

  数组的元素可以是对象,而对象的属性也可以是数组,如果把数组和对象混合在一起,那么就可以组成更为复杂的混合解构,如下所示。

({ a: [b] } = { a: [1] });
console.log(b); //

五、参数解构

  参数解构不但拥有前面两种解构的所有能力,并且能从实参对象中提取数据,赋给函数体中的同名变量。在讲解参数解构之前,先来了解一种代码优化。当函数要接收大量的参数时,为了增加维护性和扩展性,通常都会用对象代替命名参数,对象的属性就是函数的参数,如下所示。

function func1(info) {
console.log(info.name);
console.log(info.age);
}
func1({ name: "strick", age: 29 });

  虽然减少了参数的数量,但同时也降低了可读性。如果函数没有为对象的属性添加注释,那么只能通过阅读函数体中的代码来理解其含义。而换成参数解构的函数声明后,属性的含义就能一目了然,具体如下所示。

function func2({ name, age }) {
console.log(name);
console.log(age);
}
func2({ name: "strick", age: 29 });

  在上面的代码中,函数的参数是一个对象,当然,也可以把参数换成数组,利用第2篇中的剩余参数来实现相同效果的参数解构,具体如下所示。

function func3(...[name, age]) {
console.log(name);
console.log(age);
}
func3("strick", 29);

1)限制

  如果参数解构的目标是对象,那么就会有一个限制。这个限制就是调用函数必须传递该对象,否则会抛出异常。注意,下面是错误的写法。

func2();

  若要避免抛出异常,可为参数定义默认值(这是ES6新增的函数特性),如下所示。

function func4({ name, age } = {}) {
console.log(name);
console.log(age);
}
func4();

2)默认值对比

  当解构默认值和参数默认值结合应用时,两者之间会有一些微妙的差别。下面是两个函数,func5()函数包含解构默认值,func6()函数包含参数默认值。

function func5({ name = "strick" } = {}) {          //解构默认值
console.log(name);
}
function func6({ name } = { name: "freedom" }) { //参数默认值
console.log(name);
}

  下面是两组函数调用的对比,第一组省略了函数的参数,第二组传入的参数值为undefined。

func5();            //"strick"
func6();    //"freedom"
func5(undefined); //"strick"
func6(undefined); //"freedom"

  根据上面输出的结果可知,name变量被赋为解构默认值或参数默认值。这是因为如果函数的参数省略或其值为undefined,那么就会使用参数的默认值。由于func6()函数的参数指定了默认值(即定义了name属性),因此对象解构后,name变量被赋值为“freedom”。而func5()函数的参数默认值是空对象,得再根据对象解构默认值的规则才能得出name变量的值,最终name变量会被赋值为“strick”。

  下面是传入空对象的情况,两个函数中的参数都会被赋为空对象。在对象解构的时候,由于func5()函数的参数包含解构默认值,因此name变量被赋值为“strick”;而func6()函数的参数并不包含解构默认值,因此name变量的值为undefined。

func5({});         //"strick"
func6({}); //undefined