要储存和设置数据,我们一般采用最多的是单个变量,而对于多个数据、复杂类型的处理,我们则会使用数组或者对象。它们组成了目前最为流行的数据格式 - JSON。而为了更加方便的定义和处理数据,ES6 中引入了 解构(destructuring) 的概念。

解构的操作主要体现在赋值方面,它可以从数组或者对象中提取数据赋值给不同的变量。解构赋值有对象解构赋值、数组解构赋值。接下来,看看它都有哪些内容。

一、对象解构赋值

1.1 同名解构赋值

我们在获取一个对象后,需要提取对象里的属性进行操作,但为了使源对象保持不变,通常我们会用一些变量保存对象里的属性,然后再进行相关操作:

var person = {
job: 'web',
age: 28
};

var job = person.job,
age = person.age;

console.log(job, age); // 'web' 28

试想下,如果需要提取对象里的多个变量,那就需要逐个一一声明、赋值,这显然很繁琐,并且增加很多重复的代码。

针对上面的问题,ES6 引入了 对象解构赋值。可以同时声明多个变量,并且赋值,这极大的方便了数据的操作:

var person = {
job: 'web',
age: 28
};

var {job, age, email} = person;

console.log(job, age, email); // 'web' 28 undefined

其实上面的代码 {name, age} = person 相当于 {name, age} = {name: 'yix', age: 28}{name, age} = person 这个赋值表达式的左边是定义的变量,变量名与对象属性名必须相同,且一一对应。如果对象中找不到对应变量名的属性,则该变量会返回 undefined

如果你要同时改变多个已经声明赋值过的变量的值,你也可以通过以下方式:

var person = {
job: 'web',
age: 28
},
job = 'math',
age = 30;

({job, age} = person);

console.log(job, age); // 'web' 28

或许你已经注意到上面的解构代码被一个小括号括了起来,这是因为如果不用小括号的话,{name, age} 会被当成代码块处理,而这样会引发错误。

1.2 默认参数

在写插件时,为了避免用户不设定参数的情况,通常会指定若干个默认参数。而通过上面的例子我们也知道,如果声明的变量未在对象中找到,则会返回 undefined。这当然不是我们希望的,幸好,ES6 允许我们给变量指定默认值,方式如下:

var person = {
job: 'web',
age: 28
};

var {job, age, email = 'abc@126.com'} = person;

console.log(job, age, email); // 'web' 28 'abc@126.com'

1.3 异名解构赋值

这里的 异名解构赋值 是相对于上面的 同名解构赋值 而言,前面说了,解构赋值中左边的变量名必须和右边的对象属性名相同,否则无法正常获取对应的属性值。但倘若你就是不想使用同名变量,ES6 也可以满足你,只需要像下面这样:

var person = {
job: 'web',
age: 28
};

var {job: myJob, age: myAge} = person;

console.log(myJob, myAge); // 'web' 28
console.log(job); // job is not defined

上面解构代码声明了 myNamemyAge 这两个变量,而其中的 nameage 会读取 person 对象的属性,并把属性值赋值给声明的这两个变量。注意,这里是左边为 “属性值”,右边为 “属性名”,赋值是从左边到右边,而普通对象却是相反,左边属性名,右边则是属性值。

当然,你也可以在 异名解构赋值 中设置默认值:

var person = {
job: 'web',
age: 28
};

var {job: myJob, age: myAge, email: myEmail = 'abc@126.com'} = person;

console.log(myJob, myAge, myEmail); // 'web' 28 'abc@126.com'

1.4 嵌套解构

对于一些复杂的对象,我们要获取嵌套在内部很深的属性,通常需要类似 obj.property.propertySon1.propertySon1Son 这样的形式。而使用解构,则能快速获取想要的值,如:

var person = {
job: 'web',
age: 28,
course: {
math: {
point: 80,
grade: 'B'
}
}
};

var {course: {math}} = person;

console.log(math); // {point: 80, grade: 'B'}
console.log(course); // course is not defined

同样地,你也可以在 嵌套解构 中设置 异名解构赋值,只是,看起来会有些混乱:

var person = {
job: 'web',
age: 28,
course: {
math: {
point: 80,
grade: 'B'
}
}
};

var {course: {math: point}} = person;
var {course: {math: myMath}} = person;

console.log(point); // {point: 80, grade: 'B'}
console.log(myMath); // {point: 80, grade: "B"}
console.log(math); // math is not defined
console.log(course); // course is not defined

二、数组解构赋值

2.1 解构赋值

相比与对象是根据属性来解构,数组则是根据数组项(项位置)来解构。通常而言,我们需要分别写多行来定义一个变量,比如:

var name = 'yix',
age = 28,
email = ['xx@126.com', 'xx@163.com'];

但是,如果你通过数组解构的方式来声明赋值,则可以同时声明多个变量:

var info = ['yix', 28, ['xx@126.com', 'xx@163.com']];
var [name, age, email] = info;

console.log(name, age); // 'yix' 28
console.log(email); // ['xx@126.com', 'xx@163.com']

从上面的代码可以知道,左边变量定义的顺序和右边数组 info 中的数组项一一对应,并且变量的值也是对应数组项的值。我们可以尝试着调换变量顺序:

var info = ['yix', 28];
var [age, name, job] = info;

console.log(age, name, job); // 'yix' 28 undefined

由上面的例子,变量顺序调换,数组(值)不变,则第一个变量仍然是对应数组第一项,这也说明了数组的解构是取决于数组项的顺序。而对于没有对应数组项的变量,它的值就是 undefined,这与 ES5 是相同的,即 只声明、不赋值。

基于是按照顺序来解构这一规则,我们就能请轻松的交换两个变量的值:

var a = 1,
b = 2;

console.log(a, b); // 1 2

[a, b] = [b, a];

console.log(a, b); // 2 1

如果是在 ES5 时期,我们必须借助第三个变量才能完成这项工作。

或许,你会觉得这个数组解构没有对象解构方便,因为它要一一对应,这和函数中传入多个参数一样,你要判断参数的位置,采取容错措施,而如果你传入的参数是一个对象,你只需要判断对象的属性即可。

其实这点你大可不必在意,因为数组解构的变量定义中,你可以省略一些变量名称,直接取你需要数组项的值,比如,在下面的代码中,我只需要获取 email 信息,那么我可以通过省略其中的项,只定义我需要的数组项的变量:

var info = ['yix', 28, ['xx@126.com', 'xx@163.com']];
var [, , email] = info;

console.log(email); // ['xx@126.com', 'xx@163.com'];

如果你要同时改变多个已经声明赋值过的变量的值,你可以通过以下方式:

var info = ['yix', 28];
var name = 'abc',
age = 29;

console.log(name, age); // 'abc' 29

[name, age] = info;

console.log(name, age); // 'yix' 28

需要注意的是,如果解构的右边不是数组,则会出现语法报错。

2.2 默认参数

和对象类似,数组解构中也有默认参数,对于没有对应项数组值的变量来说,而已是再合适不过:

var info = ['yix'];
var [name, age = 30] = info;

console.log(name, age); // 'yix' 30

2.3 嵌套解构 和 Rest 参数

目前为止,我们只看到简单的对应关系。但实际编码过程,可能会存在一些复杂的嵌套解构,但基本原则是不变的:

var info = ['yix', ['xx@126.com', 'xx@163.com'], 28];
var [name, [firstEmail], age] = info;

console.log(name, firstEmail, age); // 'yix' 'xx@126.com' 28

假设定义的变量中存在 Rest 参数,如果 Rest 参数变量有对应的项,则 Rest 参数的值为 剩余数组项所组成的数组。如果没有,则 Rest 参数的值为一个 空数组,来看代码:

// 有对应项
var info = ['yix', 28, ['xx@126.com', 'xx@163.com'], 'web'];
var [name, age, ...restInfo] = info;

console.log(name, age, restInfo); // 'yix' 28 [['xx@126.com', 'xx@163.com'], 'web']
console.log(restInfo[0]); // ['xx@126.com', 'xx@163.com']
console.log(restInfo[1]); // 'web'

// 无对应项
var number = [2, 1];
var [a, b, ...restNumber] = number;

console.log(a, b, restNumber); // 2 1 []

三、解构特殊值

在前面,我们对对象、数组进行解构。但需要注意的是,并不是什么值都能解构,我们尝试着把解构的值的数据类型依次设置为 数字、NaN、字符串、null、undefined,看看会得到怎样的结果:

当解构的值为 数字 时,会抛出一个错误:

var info = 1;
var {name} = info;
var [age] = info;

console.log(name); // 报错 undefined is not a function
console.log(age); // 报错 undefined is not a function

当解构的值为 NaN 时,会抛出一个错误:

var info = NaN;
var {name} = info;
var [age] = info;

console.log(name); // 报错 undefined is not a function
console.log(age); // 报错 undefined is not a function

当解构的值为 字符串时,会将字符串先分解(split)成数组:

var info = 'ab';
var {name, age, job} = info;
var [name1, age1, job1] = info;

console.log(name, age, job); // undefined undefined undefined
console.log(name1, age1, job1); // 'a' 'b' undefined

当解构的值为 null时,也会抛出一个错误:

var info = null;
var {name} = info;
var [age] = info;

console.log(name); // 报错 Cannot match against 'undefined' or 'null'
console.log(age); // 报错 Cannot match against 'undefined' or 'null'

当解构的值为 undefined 时,同样抛出一个错误:

var info = undefined;
var {name} = info;
var [age] = info;

console.log(name); // 报错 Cannot match against 'undefined' or 'null'
console.log(age); // 报错 Cannot match against 'undefined' or 'null'

四、应用

4.1 复杂的数据提取

前面也说过,对于一些复杂的对象,我们要获取嵌套在内部很深的属性,无论提取的值是数组还是对象,通常需要类似 obj.property.propertySon1.propertySon1Son 这类操作。

但如果你用解构,则能轻松的提取想要的信息:

var info = {
name: 'yix',
contact: {
tel: '12345678',
email: ['xx@126.com', 'xx@163.com']
},
activityTime: {
morning: {
total: 40,
run: 10,
basketball: 30
},
night: {
total: 45,
run: 10,
badminton: 35
}
}
};

var {contact: {email: [firstEmail]}, activityTime: {night}} = info;

console.log(firstEmail, night.total); // 'xx@126.com' 45

上面的代码的目的是提取 info 对象中联系方式的 第一个邮箱,以及 晚上活动的时间 。

4.2 函数参数设置

我们经常会对函数做一些预留参数,如果用户不设置这个参数值,则在程序运行时,该参数会使用我们设定的默认值,而通常的做法,就是使用 的方式,如下面的一个简单的 ajax方法 :

function ajax(url, opt) {
var method = opt.method || 'GET';
data = opt.data || {time: new Date()/1};
dataType = opt.dataType || 'json';
cache = opt.cache || false;

// do something
}

我们会对参数对象中的一个或者多个属性设置一个默认值,每个都要设置一遍,这样显得特别繁琐。还记得前面讲到的对象中的默认参数吗?我们可以用它来改下上面的形式,这样一来,代码就变得十分简洁了:

function ajax(url, {
method = 'GET',
data = {time: new Date()/1},
dataType = 'json',
cache = false
} = {}) {

// do something
}