前言:ES6简介
ES6,全称ECMAScript 6.0
,是 JavaScript 的下一个版本标准,2015.06 发版。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ECMAScript
是JavaScript语言的国际标准,JavaScript
是ECMAScript
的实现。
ES6 经过持续几年的磨砺,它已成为 JS
有史以来最实质的升级,特性涵盖范围甚广, 小到受欢迎的语法糖,例如箭头函数(arrow functions
)和简单的字符串插值(string interpolation
),大到烧脑的新概念,例如代理(proxies
)和生成器(generators
);它将彻底改变程序员们编写JS代码的方式。
ES6 主要是为了解决 ES5 的先天不足,比如 JavaScript 里并没有类的概念,但是目前浏览器的 JavaScript 是 ES5 版本,大多数高版本的浏览器也支持 ES6,不过只实现了 ES6 的部分特性和功能。
要讲清 ECMAScript
和 JavaScript
之间的关系,就需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA(European computer manufactures association,欧洲计算机制造联合会)
,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262
)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0
版。
该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape
公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。
因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript
和 ActionScript
)。日常场合,这两个词是可以互换的。
ECMAScript 2015
(简称 ES2015
),在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 语言”。
任何人都可以向标准委员会(又称 TC39
委员会)提案,要求修改语言标准。
一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。
一个提案只要能进入 Stage 2
,就差不多肯定会包括在以后的正式标准里面。
ES6
从开始制定到最后发布,整整用了15
年。
前面提到,ECMAScript 1.0
是 1997
年发布的,接下来的两年,连续发布了 ECMAScript 2.0
(1998 年 6 月)和 ECMAScript 3.0
(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。
2000 年,ECMAScript 4.0
开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。
为什么 ES4 没有通过呢? 因为这个版本太激进了,对 ES3
做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39
)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。
2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich
为首的 Mozilla 公司,则坚持当前的草案。
2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1
,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5
。
2009 年 12 月,ECMAScript 5.0
版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next
继续开发,后来演变成 ECMAScript 6
;一些不是很成熟的设想,则被视为 JavaScript.next.next
,在更远的将来再考虑推出。TC39
委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7
。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。
2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。
2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。
2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。
2015 年 6 月,ECMAScript 6 正式通过,成为国际标准
。从 2000 年算起,这时已经过去了 15 年。
总结来说
ES6 是 ECMAScript 标准十余年来变动最大的一个版本,为其添加了许多新的语法特性。
1. let、const 和 块级 作用域
ES6 提出了两个新的声明变量的命令:let
和 const
。其中,let
完全可以取代var
,因为两者语义相同,而且let
没有副作用。因为let
具有块级作用域,所声明的变量,只在其所在的代码块内有效。
有几个点需要注意:
let
关键词声明的变量不具备变量提升(hoisting
)特性let
和 const
声明只在最靠近的一个块中(花括号内)有效const
声明时,请使用大写变量,如:CAPITAL_CASING
const
在声明时必须被赋值ES5
只有全局作用域
和函数作用域
,没有块级作用域,这带来很多不合理的场景。
块级作用域{ ...}
:就是有大括号的就算,例如 if 、for 等。
ES6以前没有块级作用域的时候,是怎么做的呢?
IIFE
)的方式来模拟。因为在函数里面是局部作用域if (true) {
//var y = 10;
let x = 'hello';
}
for (let i = 0; i < 10; i++) {
console.log(i);
}
// 10
//alert(y)
// Uncaught ReferenceError: x is not defined
alert(x)
上面代码如果用 var
替代 let
,实际上就声明了两个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效, var
命令做不到这一点。
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
同样在块级作用域有效的另一个变量声明方式是 const
,它可以声明一个常量。ES6 中,const
声明的常量类似于指针,它指向某个引用,也就是说这个「常量」并非一成不变的,如:
{
const ARR = [5,6];
ARR.push(7);
console.log(ARR); // [5,6,7]
ARR = 10; // TypeError
}
上面例子说明:当常量的值为对象得时候,不变得是其指针,但是 所指对象得本身是可以改变得。
在let
和const
之间,建议优先使用const
,尤其是在全局环境,不应该设置变量,只应设置常量。所有的函数都应该设置为常量。
const
优于 let
有几个原因。
const
可以提醒阅读程序的人,这个变量不应该改变;const
比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;防止了无意间修改变量值所导致的错误。// bad
var a = 1, b = 2, c = 3;
// good
const a = 1;
const b = 2;
const c = 3;
// best
const [a, b, c] = [1, 2, 3];
2. 箭头函数(Arrow Functions)
ES6 中,箭头函数就是函数的一种简写形式,使用括号包裹参数,跟随一个 =>,紧接着是函数体。
var getPrice = function() {
return 4.55;
};
// Implementation with Arrow Function
var getPrice = () => 4.55;
需要注意的是,上面例子中的 getPrice
箭头函数采用了简洁函数体,它不需要 return
语句,下面这个例子使用的是正常函数体:
let arr = ['apple', 'banana', 'orange'];
let breakfast = arr.map(fruit => {
return fruit + 's';
});
console.log(breakfast); // apples bananas oranges
当然,箭头函数不仅仅是让代码变得简洁,函数中 this
总是绑定总是指向对象自身。具体可以看看下面几个例子:
function Person() {
this.age = 0;
setInterval(function growUp() {
// 在非严格模式下,growUp() 函数的 this 指向 window 对象
this.age++;
}, 1000);
}
var person = new Person();
我们经常需要使用一个变量来保存 this,然后在 growUp 函数中引用:
function Person() {
var self = this;
self.age = 0;
setInterval(function growUp() {
self.age++;
}, 1000);
}
而使用箭头函数可以省却这个麻烦:
function Person(){
this.age = 0;
setInterval(() => {
// |this| 指向 person 对象
this.age++;
}, 1000);
}
var person = new Person();
3. 函数参数默认值
ES6 中允许你对函数参数设置默认值:
let getFinalPrice = (price, tax=0.7) => price + price * tax;
getFinalPrice(500); // 850
4. Spread / Rest 操作符
Spread / Rest
操作符指的是 ...
,具体是 Spread
还是 Rest
需要看上下文语境。
Spread
操作符:function foo(x,y,z) {
console.log(x,y,z);
}
let arr = [1,2,3];
foo(...arr); // 1 2 3
Rest
操作符:function foo(...args) {
console.log(args);
}
foo( 1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]
5. 对象词法扩展
ES6 允许声明在对象字面量时使用简写语法,来初始化属性变量和函数的定义方法,并且允许在对象属性
中进行计算操作
:
function getCar(make, model, value) {
return {
// 简写变量
make, // 等同于 make: make
model, // 等同于 model: model
value, // 等同于 value: value
// 属性可以使用表达式计算值
['make' + make]: true,
// 忽略 `function` 关键词简写对象函数
depreciate() {
this.value -= 2500;
}
};
}
let car = getCar('Barret', 'Lee', 40000);
// output: {
// make: 'Barret',
// model:'Lee',
// value: 40000,
// makeBarret: true,
// depreciate: [Function: depreciate]
// }
6. 二进制和八进制字面量
ES6 支持二进制和八进制的字面量,通过在数字前面添加 0o
或者0O
即可将其转换为八进制
值;二进制使用 0b
或者 0B
。
let oValue = 0o10;
console.log(oValue); // 8
let bValue = 0b10; //
console.log(bValue); // 2
7. 对象和数组解构
解构可以避免在对象赋值时产生中间变量:
function foo() {
return [1,2,3];
}
let arr = foo(); // [1,2,3]
let [a, b, c] = foo();
console.log(a, b, c); // 1 2 3
function bar() {
return {
x: 4,
y: 5,
z: 6
};
}
let {x: x, y: y, z: z} = bar();
console.log(x, y, z); // 4 5 6
8. 对象超类
ES6
允许在对象中使用 super
方法:
var parent = {
foo() {
console.log("Hello from the Parent");
}
}
var child = {
foo() {
super.foo();
console.log("Hello from the Child");
}
}
Object.setPrototypeOf(child, parent);
child.foo(); // Hello from the Parent
// Hello from the Child
9. 模板语法和分隔符
ES6
中有一种十分简洁的方法组装一堆字符串和变量。
${ ... }
用来渲染一个变量,静态字符串一律使用单引号或反引号,不使用双引号。 动态字符串使用反引号。
let user = 'Barret';
console.log(`Hi ${user}!`); // Hi Barret!
10. for…of VS for…in
let nicknames = ['di', 'boo', 'punkeye'];
nicknames.size = 3;
for (let nickname of nicknames) {
console.log(nickname);
}
// 结果: di, boo, punkeye
let nicknames = ['di', 'boo', 'punkeye'];
nicknames.size = 3;
for (let nickname in nicknames) {
console.log(nickname);
}
//Result: 0, 1, 2, size
11. Map 和 WeakMap
ES6 中两种新的数据结构集:Map
和 WeakMap
。事实上每个对象都可以看作是一个 Map
。
一个对象由多个 key-val
对构成,在 Map
中,任何类型都可以作为对象的 key
,如:
var myMap = new Map();
var keyString = "a string",
keyObj = {},
keyFunc = function () {};
// 设置值
myMap.set(keyString, "value 与 'a string' 关联");
myMap.set(keyObj, "value 与 keyObj 关联");
myMap.set(keyFunc, "value 与 keyFunc 关联");
myMap.size; // 3
// 获取值
myMap.get(keyString); // "value 与 'a string' 关联"
myMap.get(keyObj); // "value 与 keyObj 关联"
myMap.get(keyFunc); // "value 与 keyFunc 关联"
WeakMap
就是一个 Map
,只不过它的所有 key
都是弱引用,意思就是 WeakMap
中的东西垃圾回收时不考虑,使用它不用担心内存泄漏问题。
另一个需要注意的点是,WeakMap
的所有 key
必须是对象。它只有四个方法 delete(key),has(key),get(key) 和set(key, val)
:
let w = new WeakMap();
w.set('a', 'b');
// Uncaught TypeError: Invalid value used as weak map key
var o1 = {},
o2 = function(){},
o3 = window;
w.set(o1, 37);
w.set(o2, "azerty");
w.set(o3, undefined);
w.get(o3); // undefined, because that is the set value
w.has(o1); // true
w.delete(o1);
w.has(o1); // false
12. Set 和 WeakSet
Set
对象是一组不重复的值,重复的值将被忽略,值类型可以是原始类型和引用类型:
let mySet = new Set([1, 1, 2, 2, 3, 3]);
mySet.size; // 3
mySet.has(1); // true
mySet.add('strings');
mySet.add({ a: 1, b:2 });
可以通过 forEach
和 for...of
来遍历 Set
对象:
mySet.forEach((item) => {
console.log(item);
// 1
// 2
// 3
// 'strings'
// Object { a: 1, b: 2 }
});
for (let value of mySet) {
console.log(value);
// 1
// 2
// 3
// 'strings'
// Object { a: 1, b: 2 }
}
Set
同样有 delete()
和 clear()
方法。
类似于 WeakMap
,WeakSet
对象可以让你在一个集合中保存对象的弱引用,在 WeakSet
中的对象只允许出现一次:
var ws = new WeakSet();
var obj = {};
var foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false, foo 没有添加成功
ws.delete(window); // 从结合中删除 window 对象
ws.has(window); // false, window 对象已经被删除
13. 类
ES6 中有class
语法。值得注意是,这里的 class
不是新的对象继承模型,它只是原型链的语法糖表现形式。
函数中使用 static
关键词定义构造函数的的方法和属性(可理解为静态属性和方法):
class Task {
constructor() {
console.log("task instantiated!");
}
showId() {
console.log(23);
}
static loadAll() {
console.log("Loading all tasks..");
}
}
console.log(typeof Task); // function
let task = new Task(); // "task instantiated!"
task.showId(); // 23
Task.loadAll(); // "Loading all tasks.."
类中的继承和超集:
class Car {
constructor() {
console.log("Creating a new car");
}
}
class Porsche extends Car {
constructor() {
super();
console.log("Creating Porsche");
}
}
let c = new Porsche();
// Creating a new car
// Creating Porsche
extends
允许一个子类继承父类,需要注意的是,子类的constructor
函数中需要执行 super()
函数。
当然,你也可以在子类方法中调用父类的方法,如super.parentMethodName()
。
有几点值得注意的是:
hoisting
),如果你要使用某个 Class
,那你必须在使用之前定义它,否则会抛出一个 ReferenceError
的错误function
关键词14. Symbol
Symbol
是一种新的数据类型,它的值是唯一的,不可变的。ES6 中提出 symbol
的目的是为了生成一个唯一的标识符,不过你访问不到这个标识符:
var sym = Symbol( "some optional description" );
console.log(typeof sym); // symbol
注意,这里 Symbol
前面不能使用 new
操作符。
var o = {
val: 10,
[ Symbol("random") ]: "I'm a symbol",
};
console.log(Object.getOwnPropertyNames(o)); // val
symbol
属性,需要使用Object.getOwnPropertySymbols(o)
。15. 迭代器(Iterators)
迭代器允许每次访问数据集合的一个元素,当指针指向数据集合最后一个元素时,迭代器便会退出。它提供了 next()
函数来遍历一个序列,这个方法返回一个包含 done
和 value
属性的对象。
ES6 中可以通过 Symbol.iterator
给对象设置默认的遍历器,无论什么时候对象需要被遍历,执行它的 @@iterator
方法便可以返回一个用于获取值的迭代器。
数组默认就是一个迭代器:
var arr = [11,12,13];
var itr = arr[Symbol.iterator]();
itr.next(); // { value: 11, done: false }
itr.next(); // { value: 12, done: false }
itr.next(); // { value: 13, done: false }
itr.next(); // { value: undefined, done: true }
你可以通过[Symbol.iterator]()
自定义一个对象的迭代器。
16. Generators
Generator
函数是 ES6 的新特性,它允许一个函数返回的可遍历对象生成多个值。
在使用中你会看到 *
语法和一个新的关键词 yield
:
function *infiniteNumbers() {
var n = 1;
while (true){
yield n++;
}
}
var numbers = infiniteNumbers(); // returns an iterable object
numbers.next(); // { value: 1, done: false }
numbers.next(); // { value: 2, done: false }
numbers.next(); // { value: 3, done: false }
每次执行 yield
时,返回的值变为迭代器的下一个值。
17. Promises
ES6 对 Promise
有了原生的支持,一个 Promise
是一个等待被异步执行的对象,当它执行完成后,其状态会变成 resolved
或者rejected
。
var p = new Promise(function(resolve, reject) {
if (/* condition */) {
// fulfilled successfully
resolve(/* value */);
} else {
// error, rejected
reject(/* reason */);
}
});
每一个 Promise
都有一个 .then
方法,这个方法接受两个参数,第一个是处理 resolved
状态的回调,一个是处理 rejected
状态的回调:
p.then((val) => console.log("Promise Resolved", val),
(err) => console.log("Promise Rejected", err));
16. 数组
// bad
const len = items.length;
const itemsCopy = [];
let i;
for (i = 0; i < len; i++) {
itemsCopy[i] = items[i];
}
// good
const itemsCopy = [...items];
使用 Array.from 方法,将类似数组的对象转为数组
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
17. 函数
立即执行函数可以写成箭头函数的形式。
(() => {
console.log('Welcome to the Internet.');
})();
那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this
。
// bad
[1, 2, 3].map(function (x) {
return x * x;
});
// good
[1, 2, 3].map((x) => {
return x * x;
});
// best
[1, 2, 3].map(x => x * x);
箭头函数取代Function.prototype.bind
,不应再用 self/_this/that
绑定 this
。
// bad
const self = this;
const boundMethod = function(...params) {
return method.apply(self, params);
}
// acceptable
const boundMethod = method.bind(this);
// best
const boundMethod = (...params) => method.apply(this, params);
简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
// bad
function divide(a, b, option = false ) {
}
// good
function divide(a, b, { option = false } = {}) {
}
不要在函数体内使用 arguments
变量,使用 rest
运算符(...
)代替。因为 rest
运算符显式表明你想要获取参数,而且 arguments
是一个类似数组的对象,而 rest
运算符可以提供一个真正的数组。
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// good
function concatenateAll(...args) {
return args.join('');
}
使用默认值语法设置函数参数的默认值。
// bad
function handleThings(opts) {
opts = opts || {};
}
// good
function handleThings(opts = {}) {
// ...
}