ES6简介
ECMAScript 6.0(简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ES6是下一代 JavaScript 的标准,也就是说,现在的浏览器用的 JavaScript 脚本大部分是以前的版本,就会有很多怪异的难以理解的行为,而ES6就是为了让 JavaScript 更好的理解而制定的标准
ES6和ECMAScript 2015的关系
ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?
2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。
但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。
标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。
因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。
部署进度
各大浏览器的最新版本,对 ES6 的支持可以查看kangax.github.io/es5-compat-table/es6/。随着时间的推移,支持度已经越来越高了,超过 90%的 ES6 语法特性都实现了。这里,推荐我一直在用的 TypeScript ,它是微软开发的编程语言,是 JavaScript 的超集,使用 TypeScript编写,可以使用 ES6的特性,在编码的时候就会编译出一个对应的 js 文件,里面就是标准的 js代码。一个TypeScript应用可以利用已存在的JavaScript脚本。编译后的TypeScript脚本也可以在JavaScript中使用。
ES6一些有用的语法特性
let 和 const
JavaScript变量声明命令 var
很让人头疼,通过 var
定义的变量,它的作用域是在 function 或任何外部已经被声明的 function,是全域的 。
最常见的就是在循环后还能引用到值1
2
3
4
5
6
7var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上述代码中a[6]()
的值就是循环后i
的值,因为i
是var
命令声明的,在全局范围内有效,所以全局只有一个变量i
。每一次循环,i
的值都会改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
,也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的最后一轮的i
的值,就是10var
还有一种情况就是 变量提升
什么是变量提升?简单来说就是变量可以在声明之前使用,值为undefined,相当于变量定义的语句被移动到作用域的最顶部
注意:函数声明和变量定义存在变量提升,但函数表达式没有变量提升1
2
3
4
5
6/*变量提升*/
foo = 2;
var foo;
// 被隐式地解释为:
var foo;
foo = 2;
1 | catName("Chloe"); |
在代码中使用一个函数或变量,在声明该函数或变量之前,这种行为很怪,这个时候,let
的出现,解决了作用域和变量提升的问题。ES6 新增了let
命令,用来声明变量。它的用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。
上述循环用let
则不会出现问题1
2
3
4
5
6
7var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
变量提升的问题也得到解决1
2
3
4
5
6
7// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
let
实际上为 JavaScript 新增了块级作用域,有了块级作用域,就不会出现循环变量泄漏为全局变量1
2
3
4for(var i=0;i<3;i++){
//
}
console.log(i);//3
另外一个命令是const
,声明一个只读的常量,一旦声明,常量的值就不能改变,这就意味着,const
一旦声明变量,就必须立即初始化,不能留到以后再赋值1
2const a;
//Uncaught SyntaxError: Missing initializer in const declaration
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const
只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。1
2
3
4
5
6const person={};
person.age=18;
person.age;//18
person = {};
//Uncaught TypeError: Assignment to constant variable.
上面代码中,常量person
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把person
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。const
和let
的作用域相同,只在声明所在的块级作用域内有效,除此之外,ES6 还规定了暂时性死区(temporal dead zone,简称 TDZ)。如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。1
2
3
4
5
6
7
8
9
10
11if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
“暂时性死区”也意味着typeof
不再是一个百分之百安全的操作。1
2
3typeof x;
let x;
//Uncaught ReferenceError: x is not defined
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。1
2typeof y;
"undefined"
所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。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
20let [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。1
2let [foo] = [];
let [bar, foo] = [1];
以上两种情况都属于解构不成功,foo的值都会等于undefined。
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。1
2
3
4
5
6
7
8let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
上面两个例子,都属于不完全解构,但是可以成功。
如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。1
2
3
4
5
6
7// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
变量的解构赋值用途很多。
(1)交换变量的值1
2
3
4let x = 1;
let y = 2;
[x, y] = [y, x];
上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。
(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
10let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代码可以快速提取 JSON 数据的值。
(5)函数参数的默认值1
2
3
4
5
6
7
8
9
10
11jQuery.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’;这样的语句。
(6)遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for…of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const 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
如果只想获取键名,或者只想获取键值,可以写成下面这样。
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。1
const { SourceMapConsumer, SourceNode } = require("source-map");
参考:阮一峰《ECMAScript 6 入门》