# 解构与展开

back

# 解构

back

解构(解构赋值)是一种表达式,将数组或者对象中的数据赋予另一个变量。

在开发过程中,我们经常遇到这样问题,需要将对象某个属性的值赋给其它两个变量代码如下:

var person = {
  name: "xxxx",
  age: 20,
  hobby: ["football", "run"],
};

var a = person.name;
var hobby = person.bobby;
1
2
3
4
5
6
7
8

有了解构,我们可以精简代码了

var person = {
  name: "xxxx",
  age: 20,
  hobby: ["football", "run"],
};
let { a, hobby } = person;
let [first] = person.hobby;
1
2
3
4
5
6
7

# 数组解构

back

将数组中前两项的值分别赋给 f 和 s

let nums = [1, 2, 3];
let [f, s] = nums;
console.log(f); // 输出: 1
console.log(s); // 输出: 2
1
2
3
4

相当于使用了索引

f = nums[0];
s = nums[1];
1
2

作用于函数参数:

function f([first, second]: [number, number]) {
  console.log(first);
  console.log(second);
}
f(input);
1
2
3
4
5

变量值交换
注意:如果右侧数组解构赋值表达式的值为 null 或 undefined,则会导致程序抛出错误

[f, s] = [s, f];
console.log(f); // 输出: 2
console.log(s); // 输出: 1
1
2
3

忽略数组中某些元素

let [first] = [1, 2, 3, 4];
console.log(first); // 输出: 1
let [, second, , fourth] = [1, 2, 3, 4];
1
2
3

嵌套数组解构

嵌套数组解构与嵌套对象解构的语法类似,在原有的数组模式中插入另一个数组模式,即可将解构过程深入到下一个层级

let colors = ["red", ["green", "lightgreen"], "blue"];
// 随后
let [firstColor, [secondColor]] = colors;
console.log(firstColor); // "red"
console.log(secondColor); // "green"
1
2
3
4
5

在此示例中,变量 secondColor 引用的是 colors 数组中的值"green",该元素包含在数组内部的另一个数组中,
所以 seconColor两侧的方括号是一个必要的解构模式。同样,在数组中也可以无限深入去解构,就像在对象中一样

数组复制、使用...创建剩余变量

在 ES5 中,开发者们经常使用concat()方法来克隆数组

// 在 ES5 中克隆数组
var colors = [ "red", "green", "blue" ];
var clonedColors = colors.concat();
console.log(clonedColors); //"[red,green,blue]"
  concat()方法的设计初衷是连接两个数组,如果调用时不传递参数就会返回当前函数的副本
1
2
3
4
5

在 ES6 中,可以通过不定元素的语法来实现相同的目标

// 在 ES6 中克隆数组
let colors = ["red", "green", "blue"];
let [...clonedColors] = colors;
console.log(clonedColors); //"[red,green,blue]"
1
2
3
4

在这个示例中,我们通过不定元素的语法将 colors 数组中的值复制到 clonedColors 数组中

  • 注意:在被解构的数组中,不定元素必须为最后一个条目,在后面继续添加逗号会导致程序抛出语法错误
let nums = [1, 2, 3, 4];
let [f, ...rest] = nums;
console.log(f); // 输出: 1
console.log(rest); // 输出: [2,3,4]
1
2
3
4

# 对象解构

back

let o = {
  a: "foo",
  b: 12,
  c: "bar",
};
let { a, b } = o;
console.log(a); // 输出: foo
console.log(b); // 输出: 12
1
2
3
4
5
6
7
8

将对象 o.a 赋值给一个 a,o.b 赋值给 b,这里的**a,b 都是对象属性名且必须一致**,而属性 c 则会忽略。

let node = {
    type: "Identifier",
    name: "foo",
  },
  type = "Literal",
  name = 5;
// 使用解构来分配不同的值
({ type, name } = node);
console.log(type); // "Identifier"
console.log(name); // "foo"
1
2
3
4
5
6
7
8
9
10
  • **注意**一定要用一对小括号包裹解构赋值语句,JS 引擎将一对开放的花括号视为一个代码块。 语法规定,代码块语句不允许出现在赋值语句左侧,添加小括号后可以将块语句转化为一个表达式,从而实现整个解构赋值过程

解构赋值表达式的值与表达式右侧(也就是=右侧)的值相等,如此一来,在任何可以使用值的地方都可以使用解构赋值表达式

let node = {
    type: "Identifier",
    name: "foo",
  },
  type = "Literal",
  name = 5;
function outputInfo(value) {
  console.log(value === node); // true
}
outputInfo(({ type, name } = node));
console.log(type); // "Identifier"
console.log(name); // "foo"
1
2
3
4
5
6
7
8
9
10
11
12

调用 outputlnfo()函数时传入了一个解构表达式,由于 JS 表达式的值为右侧的值,因而此处传入的参数等同于 node,且变量 type 和 name 被重新赋值,最终将 node 传入 outputlnfo()函数

注意:解构赋值表达式(也就是=右侧的表达式)如果为null或undefined会导致程序抛出错误。也就是说,任何尝试读取null或undefined的属性的行为都会触发运行时错误

重命名

上述例子中声明的变量一个**a 和 b 必须和对象中属性一致**,如果想换一个名字,写法如下:

let { a: aa, b: bb } = o;
1

令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

let { a, b }: { a: string; b: number } = o;
1

默认值

let o = {
  a: "foo",
  b: undefined,
  c: "bar",
};
let { a, b = 1 } = o;
console.log(a); // 输出: foo
console.log(b); // 输出: 1
1
2
3
4
5
6
7
8

当属性 b 值为的**undefined**时,解构表达式会使用默认值
默认值可以让你在属性为 undefined 时使用缺省值:

function keepWholeObject(wholeObject: { a: string; b?: number }) {
  let { a, b = 1001 } = wholeObject;
}
1
2
3

使用解构赋值表达式时,如果指定的局部变量名称在对象中不存在,那么这个局部变量会被赋值为undefined

let node = {
  type: "Identifier",
  name: "foo",
};
let { type, name, value } = node;
console.log(type); // "Identifier"
console.log(name); // "foo"
console.log(value); // undefined
1
2
3
4
5
6
7
8

当指定的属性不存在时,可以随意定义一个默认值,在属性名称后添加一个等号(=)和相应的默认值即可

let node = {
  type: "Identifier",
  name: "foo",
};
let { type, name, value = true } = node;
console.log(type); // "Identifier"
console.log(name); // "foo"
console.log(value); // true
1
2
3
4
5
6
7
8

函数声明 解构也能用于函数声明。

type C = { a: string; b?: number };
function f({ a, b }: C): void {
  // ...
}
1
2
3
4

但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。

function f({ a = "", b = 0 } = {}): void {
  // ...
}
f();
1
2
3
4

其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道  C  的定义有一个  b  可选属性:

function f({ a, b = 0 } = { a: "" }): void {
  // ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to {a: ""}, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument
1
2
3
4
5
6

嵌套对象解构

解构嵌套对象仍然与对象字面量的语法相似,可以将对象拆解以获取想要的信息

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1,
    },
    end: {
      line: 1,
      column: 4,
    },
  },
};
let {
  loc: { start },
} = node;
console.log(start.line); // 1
console.log(start.column); // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在这个示例中,我们在解构模式中使用了花括号,其含义为在找到 node 对象中的 loc 属性后,应当深入一层继续查找 start 属性。 在上面的解构示例中,所有冒号前的标识符都代表在对象中的检索位置,其右侧为被赋值的变量名;
如果冒号后是花括号,则意味着要赋予的最终值嵌套在对象内部更深的层级中

更进一步,也可以使用一个与对象属性名不同的局部变量名

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1,
    },
    end: {
      line: 1,
      column: 4,
    },
  },
};
// 提取 node.loc.start
let {
  loc: { start: localStart },
} = node;
console.log(localStart.line); // 1
console.log(localStart.column); // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在这个版本中,node.loc.start 被存储在了新的局部变量 localStart 中。解构模式可以应用于任意层级深度的对象,且每一层都具备同等的功能

# 混合解构

back

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1,
    },
    end: {
      line: 1,
      column: 4,
    },
  },
  range: [0, 3],
};
let {
  loc: { start },
  range: [startIndex],
} = node;
console.log(start.line); // 1
console.log(start.column); // 1
console.log(startIndex); // 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 解构参数

back

解构可以用在函数参数的传递过程中,这种使用方式更特别。当定义一个接受大量可选参数的JS函数时,通常会创建一个可选对象,将额外的参数定义为这个对象的属性

// options 上的属性表示附加参数
function setCookie(name, value, options) {
  options = options || {};
  let secure = options.secure,
    path = options.path,
    domain = options.domain,
    expires = options.expires;
  // 设置 cookie 的代码
}
// 第三个参数映射到 options
setCookie("type", "js", {
  secure: true,
  expires: 60000,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

许多 JS 库中都有类似的 setCookie()函数,而在示例函数中,name 和 value 是必需参数,而 secure、path、domain 和 expires 则不然,
这些参数相对而言没有优先级顺序,将它们列为额外的命名参数也不合适,此时为 options 对象设置同名的命名属性是一个很好的选择。现在的问题是,仅查看函数的声明部分,无法辨识函数的预期参数,必须通过阅读函数体才可以确定所有参数的情况

如果将 options 定义为解构参数,则可以更清晰地了解函数预期传入的参数。解构参数需要使用对象或数组解构模式代替命名参数

function setCookie(name, value, { secure, path, domain, expires }) {
  // 设置 cookie 的代码
}
setCookie("type", "js", {
  secure: true,
  expires: 60000,
});
1
2
3
4
5
6
7
  • 必须传值的解构参数

解构参数有一个奇怪的地方,默认情况下,如果调用函数时不提供被解构的参数会导致程序抛出错误

// 出错!
setCookie("type", "js");
1
2

缺失的第 3 个参数,其值为 undefined,而解构参数只是将解构声明应用在函数参数的一个简写方法,其会导致程序抛出错误。当调用 setCookie()函数时,JS 引擎实际上做了以下这些事情

function setCookie(name, value, options) {
  let { secure, path, domain, expires } = options;
  // 设置 cookie 的代码
}
1
2
3
4

如果解构赋值表达式的右值为 null 或 undefined,则程序会报错。同理,若调用 setCookie()函数时不传入第 3 个参数,也会导致程序抛出错误

如果解构参数是必需的,大可忽略掉这些问题;但如果希望将解构参数定义为可选的,那么就必须为其提供默认值来解决这个问题

function setCookie(name, value, { secure, path, domain, expires } = {}) {
  // ...
}
1
2
3
function setCookie(
  name,
  value,
  {
    secure = false,
    path = "/",
    domain = "example.com",
    expires = new Date(Date.now() + 360000000),
  } = {}
) {
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12

在这段代码中,解构参数的每一个属性都有默认值,从而无须再逐一检查每一个属性是否都有默认值。然而,这种方法也有很多缺点。首先,函数声明变得比以前复杂了;其次,如果解构参数是可选的,那么仍然要给它添加一个空对象作为参数,否则像 setCookie("type","js")这样的调用会导致程序抛出错误

对于对象类型的解构参数,最好为其赋予相同解构的默认参数

function setCookie(
  name,
  value,
  {
    secure = false,
    path = "/",
    domain = "example.com",
    expires = new Date(Date.now() + 360000000),
  } = {
    secure: false,
    path: "/",
    domain: "example.com",
    expires: new Date(Date.now() + 360000000),
  }
) {
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

现在函数变得更加完整了,第一个对象字面量是解构参数,第二个为默认值。但是这会造成非常多的代码冗余,可以将默认值提取到一个独立对象中,并且使用该对象作为解构和默认参数的一部分,从而消除这些冗余

const setCookieDefaults = {
  secure: false,
  path: "/",
  domain: "example.com",
  expires: new Date(Date.now() + 360000000),
};
function setCookie(
  name,
  value,
  {
    secure = setCookieDefaults.secure,
    path = setCookieDefaults.path,
    domain = setCookieDefaults.domain,
    expires = setCookieDefaults.expires,
  } = setCookieDefaults
) {
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在这段代码中,默认值已经被放到 setCookieDefaults 对象中,除了作为默认参数值外,在解构参数中可以直接使用这个对象来为每一个绑定设置默认参数。使用解构参数后,不得不面对处理默认参数的复杂逻辑,但它也有好的一面,如果要改变默认值,可以立即在 setCookieDefaults 中修改,改变的数据将自动同步到所有出现过的地方

# 其他解构

back

符串解构

字符串也可以解构赋值。这是因为,字符串被转换成了一个类似数组的对象

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

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

const { length } = "hello";
console.log(length); //5
1
2

# 数值和布尔值解构

back

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

let { toString: s1 } = 123;
console.log(s1 === Number.prototype.toString); //true
let { toString: s2 } = true;
console.log(s2 === Boolean.prototype.toString); //true
1
2
3
4

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报错

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
1
2

# 展开

back

展开和解构正好相反,将一个数组展开为另一个数组,或将一个对象展开为另一个对象。

# 数组展开

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

console.log(bothPlus); // 输出:0,1,2,3,4,5
1
2
3
4
5

展开操作会对数组firstsecond浅拷贝,如果最前一页值修改,则 bothPlus 值不会改变。但是如果第一中是对象数组,改变对象内部的值,bothPlus 对应的值会改变。

# 对象展开

back

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
console.log(search); //输出:{food: "rich", price: "$$", ambiance: "noisy"}
1
2
3

展开是从做到右处理,意味着对象后面的属性会覆盖前面的如下:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
console.log(search); //输出:{food: "spicy", price: "$$", ambiance: "noisy"}
1
2
3

对象展开还有其它一些意想不到的限制。 首先,它仅包含对象   自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法

class C {
  p = 12;
  m() {}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!
1
2
3
4
5
6
7
8

扩展运算符(spread)是三个点(...)也可以创建对象(返回一个新对象),注意这是一个浅拷贝

const obj = { name: "dengke" };
const obj1 = {
  age: 18,
  temp: {
    a: 10,
  },
};

const obj2 = { ...obj, ...obj1 };
console.log(obj2); // { name: 'dengke', age: 18, temp: { a: 10 } }

obj2.temp.a = 20;
console.log(obj2); // { name: 'dengke', age: 18, temp: { a: 20 } }
console.log(obj1); // { name: 'dengke', age: 18, temp: { a: 20 } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14