ES6重点特性

1. 一些核心功能

1.1 let const

ES5 通过 var 来申明变量,ES6 新添 let 和 const,且作用域是 块级作用域。

let 使用和 var 非常类似,let 不存在变量提升,也不允许重复申明,let 的声明只能在它所在的代码块有效

const 就是申明常量用的,一旦申明即被锁定,后面无法更改。

1.2 解构赋值(Destructuring)

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

ES6 解构赋值基本语法 var [a, b, c] = [1, 2, 3];,从数组中取值,并按照先后次序来赋值。如果解构赋值不成功,就会返回 underfined,解构赋值也允许指定默认值

1
2
3
4
5
6
var [a, b] = [1];
b // undefined
// 指定默认值
var [a, b = 2] = [1];
b // 2

除了数组,对象也可以解构赋值,但是数组是有顺序的,而对象没有顺序,如果想要成功赋值,必须与对象属性同名,才能成功赋值,否则返回 underfined:

1
2
3
4
5
6
var {a, b} = {a: 1, b: 2};
a // 1
b // 2
var {a, c} = {a: 1, b: 2};
c // undefined

字符串的解构赋值比较有意思,既可以把字符串当作可以迭代的数组,又可以当作对象,比如:

1
2
3
4
5
var [a1,a2,a3,a4,a5] = 'hello';
a2 // e
var {length : len} = 'hello';
len // 5

1.3 字符串模版(template string)

当我们要插入大段的html内容到文档中时,传统的写法非常麻烦

1
2
3
4
5
6
$("#result").append(
"There are <b>" + basket.count + "</b> " +
"items in your basket, " +
"<em>" + basket.onSale +
"</em> are on sale!"
);

我们要用一堆的’+’号来连接文本与变量,而使用ES6的新特性模板字符串``后,我们可以直接这么来写:

1
2
3
4
5
$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);

1.4 set 集合和 Map 结构

set 集合

  • ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
  • Set 方法分为操作和遍历,操作方法有 add-添加成员, delete-删除成员, has-拥有判断返回布尔值, clear-清空集合。
  • 遍历操作有 keys(),values(),entries(),forEach(),…,for of,map 和 filter 函数也可以用于 Set,不过要进行巧妙操作,先转换成数组,在进行操作
1
2
3
let set = new Set([1,2,3]);
set = new Set([...set].map(a => a*2));
// Set {2, 4, 6}

Map 结构

  • Map 用来解决对象只接受字符串作为键名,Map 类似于对象,也是键值对集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • Map 可以通过 [set、 get、 has、 delete] 方法来操作:
1
2
3
4
5
6
7
8
var m = new Map();
var arr = [1, 2];
m.set(arr, 'array');
m.get(arr); // 'array'
m.has(arr) // true
m.delete(arr) // true
m.has(arr) // false

1.5 spread操作符(…)

用于函数调用:myFunction(…iterableObj);

用于数组字面量:[…iterableObj, 4, 5, 6]

函数传参

1
2
3
4
5
6
7
8
9
10
11
// Math.max()函数, 一般可以加入任意个参数
Math.max(12, 13, 14, 15); // 15
// 以数组的形式
var arr = [1, 2, 3, 4];
Math.max.apply(null, arr); // 4
// 使用 "..."
Math.max(...arr); // 4
// 还可以加入其它的一些参数
Math.max(...arr, 5, 10); // 10

数据解构

数据构造

2. 箭头函数(=>)

2.1基本用法

ES6允许使用“箭头”(=>)定义函数。

1
var f = v => v;

上面的箭头函数等同于:

1
2
3
var f = function(v) {
return v;
};

2.2使用注意点

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

2.3函数绑定

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。

函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

1
2
3
4
5
6
7
8
9
10
11
12
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}

2.4箭头函数与常规函数对比

一个箭头函数与一个普通的函数在两个方面不一样:

  • 下列变量的构造是词法的: arguments , super , this , new.target
  • 不能被用作构造函数:没有内部方法 [[Construct]] (该方法允许普通的函数通过 new 调用),也没有 prototype 属性。因此, new (() => {}) 会抛出错误。

3.Class类

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
constructor(){
this.type = 'animal'
}
says(say){
console.log(this.type + ' says ' + say)
}
}
let animal = new Animal()
animal.says('hello') //animal says hello
class Cat extends Animal {
constructor(){
super()
this.type = 'cat'
}
}
let cat = new Cat()
cat.says('hello') //cat says hello

上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的。

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Cat类,该类通过extends关键字,继承了Animal类的所有属性和方法。

super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

4.Module模块化

ES6 之前,JS 一直没有 modules 体系,解决外部包的问题通过 CommonJS 和 AMD 模块加载方案,一个用于服务器,一个用于浏览器。ES6 提出的 modules (import/export)方案完全可以取代 CommonJS 和 AMD 成为浏览器和服务器通用的模块解决方案。

关于模块,就只有两个命令,import 用于导入其他模块,export 用于输出模块。

1
2
3
4
5
6
7
8
9
10
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
// main.js
import {firstName, lastName, year} from './profile';
console.log(firstName, lastName) // Michael Jackson

export 可以输出的内容很多,包括变量、函数、类,貌似都可以输出,还可以借助 export default 来加载默认输出。

模块加载的实质

ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。

循环加载

循环加载也比较有意思,经常能看到 nodejs 中出现加载同一个模块,而循环加载却不常见,nodejs 使用 CommonJS 模块机制,CommonJS 的循环加载采用的是加载多少,输出多少,就像是我们平时打了断点一样,会跳到另外一个文件,执行完在跳回来。

5.Promise

5.1为什么产生

Promise的兴起,是因为异步方法调用中,往往会出现回调函数一环扣一环的情况。这种情况导致了回调金字塔问题的出现。不仅代码写起来费劲又不美观,而且问题复杂的时候,阅读代码的人也难以理解。例如:

1
2
3
4
5
6
7
8
9
10
db.save(data, function(data){
// do something...
db.save(data1, function(data){
// do something...
db.save(data2, function(data){
// do something...
done(data3); // 返回数据
})
});
});

假设有一个数据库保存操作,一次请求需要在三个表中保存三次数据。那么我们的代码就跟上面的代码相似了。这时候假设在第二个db.save出了问题怎么办?基于这个考虑,我们又需要在每一层回调中使用类似try…catch这样的逻辑。这个就是万恶的来源,也是node刚开始广为诟病的一点。

另外一个缺点就是,假设我们的三次保存之间并没有前后依赖关系,我们仍然需要等待前面的函数执行完毕, 才能执行下一步,而无法三个保存并行,之后返回一个三个保存过后需要的结果。(或者说实现起来需要技巧)

5.2解决回调深渊Promise

Promise对象是一个有限状态机。它三个状态:

  • Pending(进行中)
  • Resolved(已完成,又称 Fulfilled))
  • Rejected(已失败)

状态转换关系为:pending->fulfilled,pending->rejected。

Promise形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var promise = new Promise(function func(resolve, reject){
// do somthing, maybe async
if (success){
return resolve(data);
} else {
return reject(data);
}
});
promise.then(function(data){
// do something... e.g
console.log(data);
}, function(err){
// deal the err.
})

Promise数据流动

promise的then方法依然能够返回一个Promise对象,这样我们就又能用下一个then来做一样的处理。

  • 假设第一个then的第一个回调没有返回一个Promise对象,那么第二个then的调用者还是原来的Promise对象,只不过其resolve的值变成了第一个then中第一个回调函数的返回值。
  • 假设第一个then的第一个回调函数返回了一个Promise对象,那么第二个then的调用者变成了这个新的Promise对象,第二个then等待这个新的Promise对象resolve或者reject之后执行回调。
  • 如果任意地方遇到了错误,则错误之后交给遇到的第一个带第二个回调函数的then的第二个回调函数来处理。可以理解为错误一直向后reject, 直到被处理为止。

控制并发的Promise

Promise有一个”静态方法”——Promise.all(注意并非是promise.prototype), 这个方法接受一个元素是Promise对象的数组。

将其他对象变为Promise对象

Promise.resovle方法,可以将不是Promise对象作为参数,返回一个Promise对象。

6.Generator

7.async

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

7.1async函数对 Generator 函数的改进

内置执行器

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

更好的语义

async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

更广的适用性

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

返回值是 Promise

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

7.2async 函数与 Promise、Generator 函数的比较

假定某个 DOM元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

Promise 的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
var ret = null;
// 新建一个空的Promise
var p = Promise.resolve();
// 使用then方法,添加所有动画
for(var anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}

虽然 Promise的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then、catch等等),操作本身的语义反而不容易看出来

Generator 函数的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}

上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。

async 函数的写法

1
2
3
4
5
6
7
8
9
10
11
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}

可以看到Async函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将Generator写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用Generator写法,自动执行器需要用户自己提供。

参考链接1
参考链接2
参考链接3