阮一峰ES6教程读书笔记(一)解构赋值、let和const
About
读完阮一峰大神ES5教程后自觉获益匪浅,遂拜读其ES6教程。为记录所感所得,打算写《阮一峰ES6教程读后感》系列文章,有兴趣的朋友可以关注一下。
一、详解let和const命令
let
和const
是ES6
新增的变量声明命令,他们的用法与var
类似,但是经它们声明的变量与var
声明的变量有很大的区别。
(一)var和let的区别
-
var
声明的变量的作用域为全局作用域或者函数作用域,而let
声明的变量多一个块级作用域 -
var
声明的变量可以进行变量提升,即在声明变量之前使用变量,而let
声明的变量必须要在变量声明语句之后使用,否则会报错 -
let
声明的变量存在“暂时性死区”,即从区块开始至变量声明语句之前是无法访问变量的 -
let
声明的变量不允许重复声明
1. 块级作用域
我们先看看这个例子:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
其实我们预期的答案是6
,但是如果这样写代码,我们无法的到预期的答案,因为a[6]
里面存储的函数语句中的i
指向的是一个全局变量,当for
循环完毕后,i
的值为10,即函数语句中的每一个i
都指向了同一个地址,而这个地址中存储的数字为10
,所以我们无论通过数组a
中哪一个元素中的函数去打印i
得到的答案都是10
为了得到我们预期的答案,我们应该将代码进行一定的改写:
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
可以看到这里面的for
循环的i
是通过let声明的,所以i
仅在当前循环中有效,在其他循环中无法访问到当前循环的i
,我们可以理解为每一个i
是独立的,所以数组a
中每一个元素中存储的函数语句中的i
指向的是不同的地址,每个地址存储的是当前循环中的i
值
2. 不存在变量提升
正常的逻辑是我们必须先声明一个变量,然后再去使用,如果在没有声明就去使用而仅仅告诉我undefined
,这显然不能让我们满意。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
通过上面的代码我们可以发现,使用let
声明的变量终于纠正了这一点,我们无法在声明变量之前去使用它。
3. 暂时性死区(temporal dead zone,简称 TDZ)
正因为使用let
声明的变量不存在变量提升现象,所以会出现暂时性死区的现象
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
tmp = '2333'
console.log(tmp) // 2333
}
尽管我们知道在使用大括号括起来的代码块内使用let
声明变量,该变量的作用域为块级作用域,但是该块级作用域并非整个区块,从该区块开始到声明变量之前的区域被称之为“暂时性死区”,因为在这一区域内我们无法访问到后面声明的变量。
4. 不允许重复声明
在使用let
声明的变量的作用域内,不允许重复声明,无论你用let
,var
还是const
。在函数内也一样:
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
(二)块级作用域与函数声明
ES5
规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。比如:
// 在使用ES5标准的浏览器中
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}()); // 'I am inside!'
因为ES5
中,函数也是“一等公民”具有变量提升的作用,但是函数的变量提升不仅仅是函数名的变量提升,而是包括整个函数体的提升,所以上述代码可翻译为:
// 在使用ES5标准的浏览器中
function f() { console.log('I am outside!'); }
(function () {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}()); // 'I am inside!'
但是在使用ES6
标准的浏览器中,上述代码就会报错ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
所以上述代码可翻译为:
// 在使用ES6标准的浏览器中
function f() { console.log('I am outside!'); }
(function () {
var f // 此时f为undefined
if (false) {
}
f();
}()); // Uncaught TypeError: f is not a function
(三)const命令
let
和const
的用法差不多,只不过是const
一般被用来声明常量,即声明语句中必须赋值,并且声明之后不能更改
1. const命令的本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
(四)顶层对象的属性
在ES6
以前的标准中,我们使用var
和function
声明的全局变量会变成顶层对象的属性,ES6
为了改变这一点,一方面规定,为了保持兼容性,var
命令和function命
令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6
开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
二、变量的解构赋值
ES6
允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
(一)基本用法
let [a, b, c] = [1, 2, 3]; // a = 1, b = 2, c = 3
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值
1. 解构成功
解构成功的条件是=
两边的模式完全相同
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 // []
2. 不完全解构
因为我们使用解构的目的是为了给=
左边的变量赋值,那么如果从左到右都能找到对应元素,那么就能完成解构,如果此时从右到左无法完成一一对应,那么就称之为不完全解构,例如:
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
3. 解构失败
如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
(二)解构表达式默认赋值
同声明函数一样,我们可以在函数表达式中声明实参时给实参赋默认值,如果调用该函数时,形参为空,那么实参就采用声明函数时的默认值。解构赋值表达式同理
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x, y = 'b'] = ['a', 'c']; // x='a', y='c'
(三)对象的解构赋值
与数组一样,解构也可以用于嵌套结构的对象。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
此时的p
仅仅是一个模式,如果要对p
赋值,必须这么写:
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
(四)字符串解构赋值和布尔值解构赋值
1. 字符串解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
2. 布尔值解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
(五)解构赋值的作用
1. 交换变量的值
以前,这个功能只在Python中使用过,如今可以在JavaScript中使用还是很开心的
[x, y] = [y, x]
2. 从函数返回多个值
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
3. 函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
4. 提取JSON数据
这个太有用了,因为我们从接口中获得的数据一般都是JSON对象,所以使用结构赋值可以使我们的代码更简洁
const {ret, data} = res.data
5. 遍历Map结构
任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。这个在Vue中也是经常使用
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
6. 输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");