JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现,另外的 ECMAScript 方言还有 JScript 和 ActionScript

Babel转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。

1
2
3
4
5
6
7
// 转码前
input.map(item => item + 1);

// 转码后
input.map(function (item) {
return item + 1;
});

上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。

Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如IteratorGeneratorSetMapProxyReflectSymbolPromise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

Babel 也可以用于浏览器环境,使用@babel/standalone模块提供的浏览器版本,将其插入网页。

1
2
3
4
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>

注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。

Babel 提供一个REPL 在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。

let 和 const 命令

let

基本用法

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

1
2
3
4
5
6
7
{
let a = 10;
var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

for循环的计数器,就很合适使用let命令。

1
2
3
4
5
6
for (let i = 0; i < 10; i++) {
// ...
}

console.log(i);
// ReferenceError: i is not defined

上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

下面的代码如果使用var,最后输出的是10

1
2
3
4
5
6
7
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

1
2
3
4
5
6
7
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6

上面代码中,变量ilet声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

1
2
3
4
5
6
7
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)。

不存在变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined

let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

1
2
3
4
5
6
var tmp = 123;

if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

1
2
typeof x; // ReferenceError
let x;

上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

1
typeof undeclared_variable // "undefined"

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

1
2
3
4
5
6
7
8
9
10
11
// 报错
function func() {
let a = 10;
var a = 1;
}

// 报错
function func() {
let a = 10;
let a = 1;
}

块级作用域

为什么需要块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域。

第一种场景,内层变量可能会覆盖外层变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var tmp = new Date();

function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}

f(); // undefined
//相当于
var tmp = new Date();

function f() {
var tmp
console.log(tmp);
if (false) {
tmp = 'hello world';
}
}

f(); // undefined
上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。

1
2
3
4
5
6
7
var s = 'hello';

for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

ES6的块级作用域

ES6 允许块级作用域的任意嵌套。

1
2
3
4
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};

上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。

内层作用域可以定义外层作用域的同名变量。

1
2
3
4
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。

块级作用域与函数声明

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。

1
2
3
4
5
6
7
8
9
10
11
// 情况一
if (true) {
function f() {}
}

// 情况二
try {
function f() {}
} catch(e) {
// ...
}

上面两种情况实际都能运行,不会报错。

ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

总结:应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

注意:ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

const

基本用法

const声明一个只读的常量。一旦声明,常量的值就不能改变。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

1
2
const foo;
// SyntaxError: Missing initializer in const declaration

作用域

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

1
2
3
4
5
if (true) {
const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了

1
2
3
4
5
6
7
8
const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

Object.freeze

如果真的想将对象冻结,应该使用Object.freeze方法。

1
2
3
4
5
const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

1
2
3
4
5
6
7
8
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};

声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

1
2
3
4
5
window.a = 1;
a // 1

a = 2;
window.a // 2

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。

从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

1
2
3
4
5
6
7
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回undefined

globalThis

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this关键字,但是有局限性。

  • 全局环境中,this会返回顶层对象。但是,Node.js 模块中this返回的是当前模块,ES6 模块中this返回的是undefined
  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined

引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this

变量的解构赋值

数组的解构赋值

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

以前,为变量赋值,只能直接指定值。

1
2
3
let a = 1;
let b = 2;
let c = 3;

ES6 允许写成下面这样。

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

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
//如果解构不成功,变量的值就等于undefined。

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

1
2
let [foo] = [];
let [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined

不完全解构

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

1
2
3
4
5
6
7
8
let [x, y] = [1, 2, 3];
x // 1
y // 2

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

对于 Set 结构,也可以使用数组的解构赋值。

1
2
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

生成器函数(Generator Function)实现的斐波那契数列生成器

使用生成器函数(Generator Function)实现的斐波那契数列生成器。让我解释一下每一部分的作用:

1
2
3
4
5
6
7
8
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}

这是一个生成器函数 fibs,使用 function* 声明。在函数内部,定义了两个变量 ab,初始化为斐波那契数列的起始值。然后,使用一个无限循环 while (true),通过 yield a 语句产生当前的斐波那契数列的值 a。接着,使用解构赋值 [a, b] = [b, a + b]; 更新 ab 的值,实现斐波那契数列的递进。

1
let [first, second, third, fourth, fifth, sixth] = fibs();

这一行代码通过解构赋值,从生成器函数 fibs 中取得前六个斐波那契数列的值。生成器函数会持续产生数列的值,但解构赋值只会取所需数量的值。

1
2

sixth // 5

最后一行输出了 sixth,即斐波那契数列中的第六个值,结果为 5。因为斐波那契数列的前几个值是 0, 1, 1, 2, 3, 5,所以第六个值是 5

默认值

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

只有当一个数组成员严格等于undefined,默认值才会生效。

1
2
3
4
5
let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

下面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

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

let [x = 1] = [null];
x // null

惰性求值

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

1
2
3
4
5
function f() {
console.log('aaa');
}

let [x = f()] = [1];

上面代码中,因为x能取到值,所以函数f根本不会执行。

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。(有点抽象,认真看)

1
2
3
4
let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined

对象的解构赋值

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

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

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;

而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

如果解构失败,变量的值等于undefined

1
2
let {foo} = {bar: 'baz'};
foo // undefined

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

1
2
3
4
5
6
// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。

难点:

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

1
2
3
4
5
6
7
8
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
//左边的baz为变量名,foo为匹配模式,在右边没有一致的的属性名,所以要在左边使用 (匹配模式:变量名)的形式进行解构赋值
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
//对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

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

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

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

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

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

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

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。

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

let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]

默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

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

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

var {x = 3} = {x: null};
x // null

上面代码中,属性x等于null,因为nullundefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。

注意:

(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。

1
2
3
4
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

1
2
3
// 正确的写法
let x;
({x} = {x: 1});

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。

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

1
2
3
4
5
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
//不完全解构

上面代码对数组进行对象解构。数组arr0键对应的值是1[arr.length - 1]就是2键,对应的值是3

字符串的解构赋值

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

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

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

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

数值和布尔值的解构赋值

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

1
2
3
4
5
6
let {toString: s} = 123;
//相当于 let {toString: s} = {toString:123};
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

函数参数的解构赋值

1
2
3
4
5
function add([x, y]){
return x + y;
}

add([1, 2]); // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy。对于函数内部的代码来说,它们能感受到的参数就是xy

默认值

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

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

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

undefined就会触发函数参数的默认值。

1
2
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

圆括号问题

对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

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

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

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

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

用途

(1)交换变量的值

1
2
3
4
let x = 1;
let y = 2;

[x, y] = [y, x];

上面代码交换变量xy的值,这样的写法不仅简洁,而且易读,语义非常清晰。

(2)取出函数返回的多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回一个数组

function example() {
return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();

(3)函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来。

1
2
3
4
5
6
7
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

(4)提取 JSON 数据

解构赋值对提取 JSON 对象中的数据,尤其有用。

1
2
3
4
5
6
7
8
9
10
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

(5)遍历 Map 结构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

1
2
3
4
5
6
7
8
9
10
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
//解构赋值
console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想获取键名,或者只想获取键值,可以写成下面这样。

1
2
3
4
5
6
7
8
9
// 获取键名
for (let [key] of map) {
// ...
}

// 获取键值
for (let [,value] of map) {
// ...
}

(6)输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

1
const { SourceMapConsumer, SourceNode } = require("source-map");

总结

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