es6
1. let,const命令 和结构赋值
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
场景1:内层变量可能会覆盖外层变量。
场景2:用来计数的循环变量泄露为全局变量。
let
是 JavaScript 新增的块级作用域:
ES6 允许块级作用域的任意嵌套
内层作用域可以定义外层作用域的同名变量
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
1.1 let命令
ES6
新增了let
,用来声明变量,用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。for
循环的计数器,就很合适使用let
命令。注意:for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。1
2
3
4
5for (let i = 0; i < 2; i++) {
let i = 'abc';
console.log(i); // abc abc
}
// 上面代码输出了 2 次abc 表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。注意事项:
不存在变量提升
1
2
3
4
5
6
7
8
9// var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;暂时性死区(temporal dead zone,简称 TDZ)
1
2
3
4
5
6
7// 在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
var a = 123;
if (true) {
a = 'abc'; // ReferenceError
let a;
}
// 只要块级作用域内存在let命令, 它所声明的变量就绑定在这个区域,不在受外部影响。上面的代码中存在全局变量a,但是块级作用域内let又声明了一个局部变量a,导致后者绑定这个块级作用域,所以在let声明变量前,对a赋值会报错。ES6
规定暂时性死区和let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在ES5
是很常见的,现在有了这种规定,避免此类错误就很容易了。不允许重复声明
1
2
3
4
5
6
7
8
9
10
11
12// let不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
1.2 const命令
const
声明一个只读的常量。一旦声明,常量的值就不能改变。const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。const
声明的常量,也与let
一样不可重复声明。注意:
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。1
2
3
4
5
6
7const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
// 上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
1.3 解构赋值
ES6
允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
1.3.1 不同对象的解构赋值
数组的解构赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
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
let [foo] = [];
console.log(foo); // undefined
// 解构赋值允许指定默认值
let [foo=true] = [];
console.log(foo); // true对象的解构赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo, baz } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
baz // undefined
// 如果变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
// 对象的解构也可以指定默认值。默认值生效的条件是,对象的属性值严格等于undefined。
var {x = 3} = {};
x // 3
// 注意:如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 错误的写法
let x;
{x} = {x: 1}; // SyntaxError: syntax error
// 正确的写法
let x;
({x} = {x: 1});字符串的解构赋值
1
2
3
4
5
6
7
8
9// 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c] = 'hel';
a // "h"
b // "e"
c // "l"
// 类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5函数参数的解构赋值
1
2
3
4
5// 函数的参数也可以使用解构赋值。函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
1.3.2 解构赋值的用途
交换变量的值
1
2let x = 1, y = 2;
[x, y] = [y, x];从函数返回多个值
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 函数只能返回一个值,如果要返回多个值,只能放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
function example() {
return [1, 2, 3]; // 返回一个数组
}
let [a, b, c] = example();
function example() {
return { // 返回一个对象
foo: 1,
bar: 2
};
}
let { foo, bar } = example();函数参数的定义
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});提取 JSON 数据
1
2
3
4
5
6
7
8// 解构赋值对提取 JSON 对象中的数据,尤其有用。
let json = {
id: 1,
name: 'tom',
data: [20]
}
let {id, name, data} = json;
console.log(id, name, data); // 1 "tom" [20]函数参数的默认值
1
2
3
4
5
6
7
8
9
10
11
12jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
// 指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。遍历 Map 结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
// 获取键和值
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// 获取键名
for (let [key] of map) {
console.log(key)
}
// 获取键值
for (let [,value] of map) {
console.log(value)
}输入模块的指定方法
1
2// 加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");