let & const
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
1
2
3
4
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // ReferenceError: i is not defined
var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。let命令所声明的变量一定要在声明后使用,否则报错。
const声明一个只读的常量。一旦声明,常量的值就不能改变。
变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
1
let [a, b, c] = [1, 2, 3];
解构赋值允许指定默认值。
1
let [x, y = 'b'] = ['a']; // x='a', y='b'
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
1
const [a, b, c, d, e] = 'hello';
变量的解构赋值用途:
-
交换变量的值
1 2 3
let x = 1; let 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();
-
提取 JSON 数据
1 2 3 4 5 6 7 8
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309]
-
遍历 Map 结构
const 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
字符串
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
1
2
3
4
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
函数的扩展
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
1
2
3
4
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
箭头函数
ES6 允许使用“箭头”(=>)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
1
2
3
4
5
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
箭头函数的一个用处是简化回调函数。
1
2
3
4
5
6
7
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
数组
-
find()
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。1 2
[1, 4, -5, 10].find((n) => n < 0) // -5
-
fill()
fill方法使用给定值,填充一个数组。1 2
new Array(3).fill(7) // [7, 7, 7]
-
includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,1 2
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false
set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
1
2
3
4
5
6
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
方法:
add(value), delete(value), has(value), clear()
1
2
3
4
5
6
7
s.add(1).add(2).add(2); // 注意2被加入了两次
s.size // 2
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
Array.from方法可以将 Set 结构转为数组。
1
2
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
去除数组重复成员:
1
2
3
4
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
Map
Map是键值对的集合,“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
1
2
3
4
5
6
7
8
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
Map构造函数接手数组或set作为参数
1
2
3
4
5
6
7
8
9
10
11
12
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
Promise
ES6 将Promise写进了语言标准,统一了用法,原生提供了Promise对象。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
下面是一个Promise对象的简单例子。
1
2
3
4
5
6
7
8
9
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
加载图片
1
2
3
4
5
6
7
8
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
Generator函数
Generator 函数是 ES6 提供的一种异步编程解决方案. 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
1
2
3
4
5
6
7
8
9
10
11
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next()); // { value: 'hello', done: false }
console.log(hw.next()); // { value: 'world', done: false }
console.log(hw.next()); // { value: 'ending', done: true }
console.log(hw.next()); // { value: undefined, done: true }
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
1
2
3
function* gen() {
yield 123 + 456;
}
Generator的应用:
-
异步操作的同步化表达
Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next()
// 卸载UI loader.next()
第一次调用loadUI
函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next
方法,则会显示Loading界面(showLoadingScreen
),并且异步加载数据(loadUIDataAsynchronously
)。等到数据加载完成,再一次使用next
方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。
异步
所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
ES6 诞生以前,异步编程的方法,大概有下面四种。
1
2
3
4
回调函数
事件监听
发布/订阅
Promise 对象
Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。
回调函数
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
1
2
3
4
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
上面代码中,readFile函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。
Promise
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。
1
2
3
4
5
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为”回调函数地狱”(callback hell)。
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
Generator函数
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。
async 函数
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async函数对 Generator 函数的改进,体现在以下四点。
-
内置执行器
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。 - 更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。 - 更广的适用性。
- 返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
1
2
3
4
5
6
7
8
9
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
另一个例子,指定多少毫秒后输出一个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 2000).then(function(){
console.log("execute end");
});
console.log("Main");
// Main
// hello world
// execute end
Class
ES6 引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
1
2
3
4
5
6
7
8
9
10
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
1
2
3
4
5
6
7
8
9
10
11
12
class Point {
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
子类必须在constructor方法中调用super方法,否则新建实例时会报错。
Module
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。
1
2
3
4
5
6
7
8
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块是通过export命令显式指定输出的代码,再通过import命令输入。
1
2
3
4
5
6
7
8
9
10
11
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
export { firstName, lastName, year };
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
1
import { lastName as surname } from './profile.js';
import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。