ES6的那些事

let 和 const 命令

https://vgbhfive.cn/JS%20%E4%B8%AD%E7%9A%84var%E3%80%81let%E5%92%8Cconst%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E7%9B%B8%E5%90%8C/

变量的解构赋值

数组的解构赋值

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

1
2
3
4
5
6
// 从前
let a = 1;
let b = 2;
let c = 3;
// es6
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
20
21
22
23
24
25
26
27
28
29
30
31
32
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 如果解构不成功,变量的值就等于undefined。
z // []

// 对于Set 结构,也可以使用数组的解构赋值
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

// 默认值
let [x, y = 'b'] = ['a'];
x // 'a'
y // 'b'
let [x, y = 'b'] = ['a', undefined];
x // 'a'
y // 'b' ES6 内部使用严格相等运算符(===),判断一个位置是否有值。 所以,只有当一个数组成员严格等于undefined,默认值才会生效。

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

1
2
3
4
5
6
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

例子:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
let { log, sin, cos } = Math;

const { log } = console;
log('hello') // hello

// 变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

// 上面的简写
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined -> 真正被赋值的是后者,而不是前者。

// 与数组一样,解构也可以用于嵌套结构的对象,解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}

// 默认值
var {x: y = 3} = {};
y // 3
var {x = 3} = {x: undefined};
x // 3 默认值生效的条件是,对象的属性值严格等于undefined。

注意点

  1. 如果要将一个已经声明的变量用于解构赋值,必须非常小心。

    1
    2
    3
    4
    5
    // 错误的写法
    let x;
    {x} = {x: 1};
    // ({x} = {x: 1});
    // SyntaxError: syntax error

    JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

  2. 解构赋值允许等号左边的模式之中,不放置任何变量名。

    1
    2
    3
    ({} = [true, false]);
    ({} = 'abc');
    ({} = []); // 表达式虽然没有意义,但是可以执行。
  3. 由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

    1
    2
    3
    4
    let arr = [1, 2, 3];
    let {0 : first, [arr.length - 1] : last} = arr;
    first // 1
    last // 3

字符串的结构

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

1
2
3
4
5
6
7
8
9
let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

// 所以遇见null 和undefined 就会报错,因为无法转化为对象
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

函数的参数也可以使用解构赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function add([x, y]){
return x + y;
}

add([1, 2]); // 3
// 函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

// 默认值
function move({x = 0, y = 0} = {}) {
return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题是,如果模式中出现圆括号怎么处理。
ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

1
2
3
4
5
6
7
8
9
10
11
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

用途

  1. 交换变量的值

    1
    2
    3
    4
    let x = 1;
    let y = 2;

    [x, y] = [y, x];
  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
    // 参数是一组有次序的值
    function f([x, y, z]) { ... }
    f([1, 2, 3]);
  4. 提取对象(JSON) 中的数据

    1
    2
    3
    4
    5
    6
    7
    let jsonData = {
    id: 42,
    status: "OK",
    data: [867, 5309]
    };

    let { id, status, data: number } = jsonData;
  5. 函数参数的默认值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    jQuery.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
    Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
  7. 获取模块的指定方法
    加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

    1
    const { SourceMapConsumer, SourceNode } = require("source-map");

数组的扩展

  1. 扩展运算符
    扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
    1
    2
    console.log(...[1, 2, 3])
    // 1 2 3

应用:

  • 复制数组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const a1 = [1, 2];
    const a2 = a1;
    a2[0] = 2;
    a1 // [2, 2]

    const a1 = [1, 2];
    const a2 = [...a1];
    a2[0] = 2;
    a1 // [1, 2]
  • 合并数组
    1
    2
    3
    4
    const arr1 = ['a', 'b'];
    const arr2 = ['c'];
    [...arr1, ...arr2]
    // [ 'a', 'b', 'c' ]
  • 与解构赋值结合
    1
    2
    3
    const [first, ...rest] = [1, 2, 3, 4, 5];
    first // 1
    rest // [2, 3, 4, 5]
  1. Array.from()
    Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
1
2
3
4
5
6
7
8
9
10
11
12
13
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr1 = Array.from(arrayLike); // ['a', 'b', 'c']

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
  1. Array.of()
    Array.of方法用于将一组值,转换为数组。
1
Array.of(3, 11, 8) // [3,11,8]
  1. 数组实例的 find() 和 findIndex()
    数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
1
2
3
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

1
2
3
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
  1. 数组实例的 fill()
    fill方法使用给定值,填充一个数组。
1
2
3
4
5
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

Symbol

基础使用

ES5 中的对象属性名都是字符串,因此会有很大的概率造成对象命名的冲突。所以在ES6 中出现了一种解决办法来解决这个问题。

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol 值通过Symbol 函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let x = Symbol()
typeof x // "symbol"

let y = Symbol('foo')
y // Symbol('foo')
y.toString() // "Symbol(foo)"
y.description // "foo"

const obj = {
toString() {
return 'abc';
}
};
const z = Symbol(obj);
z // Symbol(abc)

let n = Symbol("foo")
n == y // false

"your symbol is " + n // TypeError: can't convert symbol to string
"your symbol is " + n.toString()

Boolean(n)
!n // false
  • Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
  • Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
  • 如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。 ES2019 提供了一个实例属性description,直接返回 Symbol 的描述。
  • Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
  • Symbol 值不能与其他类型的值进行运算,会报错。
  • Symbol 值也可以转为布尔值,但是不能转为数值。

属性名的Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

示例:消除魔法字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getArea(shape, options) {
let area = 0;

switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}

return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 消除魔术字符串
const shapeType = {
triangle: 'Triangle'
};

/**
* const shapeType = {
* triangle: Symbol()
* };
* 除了将shapeType.triangle的值设为一个 Symbol,其他地方都不用修改。
*/

function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });

上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。

Set 和 Map 数据结构

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

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
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

const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

[...new Set(array)] // 去除数组的重复成员

[...new Set('ababbc')].join('') // "abc" 去除字符串里面的重复字符。

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
  • Set本身是一个构造函数,用来生成 Set 数据结构。
  • Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
  • 向 Set 实例添加了两次NaN,但是只会加入一个。这表明,在 Set 内部,两个NaN是相等的。
  • 由于两个对象不相等,所以被视为两个值。

Set 实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。
  1. 遍历操作
    Set 结构的实例有四个遍历方法,可以用于遍历成员。
  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

需要特别指出的是,Set的遍历顺序就是插入顺序。

keys(),values(),entries()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

foreach()
Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。

1
2
3
4
5
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

遍历的应用
扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。

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
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']

let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2)); // map
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0)); // filter
// 返回Set结构:{2, 4}

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]); // 并集
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x))); // 交集
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x))); // 差集
// Set {1}

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  • WeakSet 的成员只能是对象,而不能是其他类型的值。
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。

Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。
因此在ES6 中提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false

const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

map.set(1, 'aaa').set(1, 'bbb');
map.get(1) // "bbb"

map.set(['a'], 555);
map.get(['a']) // undefined
  • 展示Map 的基础使用方法。
  • 任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。
  • 对键连续赋值两次,后一次的值覆盖前一次的值。
  • set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。

Map 的属性和操作方法

  1. size
    size属性返回 Map 结构的成员总数。

  2. Map.prototype.set(key, value)
    set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。(可以采用链式写法)

  3. Map.prototype.get(key)
    get方法读取key对应的键值,如果找不到key,返回undefined。

  4. Map.prototype.has(key)
    has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

  5. Map.prototype.delete(key)
    delete方法删除某个键,返回true。如果删除失败,返回false。

  6. Map.prototype.clear()
    clear方法清除所有成员,没有返回值。

Map 的遍历方法

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

Map 的遍历顺序就是插入顺序。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);

for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

[...map.entries] // Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
); // 借助数组的map() 和filter() 函数来实现特殊功能

map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
}); // Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

Promise 对象

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点:

  • 对象的状态不受外界影响。
    Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    Promise对象的状态改变,只有两种可能:从pending 变为fulfilled 和从pending 变为rejected 。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
    如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

基本用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

1
2
3
4
5
6
7
8
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

1
2
3
4
5
promise.then(function(value) {
// success
}, function(error) {
// failure
});

then方法可以接受两个回调函数作为参数。 其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

  • 第一个回调函数是Promise对象的状态变为resolved时调用。
  • 第二个回调函数是Promise对象的状态变为rejected时调用。

示例:

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);
}); // timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。
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
30
31
32
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();

});

return promise;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
// getJSON是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。
// 需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。
// 如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。

Promise 的方法

  1. Promise.prototype.then()

  2. Promise.prototype.catch()

  3. Promise.prototype.finally()

Generator 函数语法

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数是什么?一句话,它就是 Generator 函数的语法糖。

有一个 Generator 函数,依次读取两个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');

const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};

const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

上面代码的函数gen可以写成async函数,就是下面这样。

1
2
3
4
5
6
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点。

  • 内置执行器。
    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
  • 更好的语义。
    async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性。
    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  • 返回值是 Promise。
    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

顶层await

根据语法规格,await命令只能出现在 async 函数内部,否则都会报错。

使用要点

  1. await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中。

    1
    2
    3
    4
    5
    6
    7
    async function myFunction() {
    try {
    await somethingThatReturnsAPromise();
    } catch (err) {
    console.log(err);
    }
    }
  2. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

    1
    2
    3
    4
    5
    6
    7
    8
    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
  3. await命令只能用在async函数之中,如果用在普通函数,就会报错。

  4. async 函数可以保留运行堆栈。

    1
    2
    3
    4
    5
    6
    7
    8
    const a = () => {
    b().then(() => c());
    };

    const a = async () => {
    await b();
    c();
    };

async 实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

1
2
3
4
5
6
7
8
9
10
11
async function fn(args) {
// ...
}

// 等同于

function fn(args) {
return spawn(function* () {
// ...
});
}

Module

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

个人备注

此博客内容均为作者学习与官方文档所做笔记,侵删!
若转作其他用途,请注明来源!