Nancy's Studio.

ES6常用新特性整理

Word count: 6,623 / Reading time: 29 min
2019/08/06 Share

ES6常用新特性整理

let

let与var的不同

  1. let所声明的变量只在所在的块级作用域有效。比如花括号内,还有for循环每轮循环的i都是新的变量,而且设置循环变量的部分是父作用域,而循环体内部是单独的子作用域,两者也是互不干扰的。
  2. let不存在变量提升,变量必须要在声明之后才能使用。
  3. 只要块级作用域内使用let声明了变量,那么这个变量就被绑定到这个区域,不受外部影响。
  4. 不允许重复声明。

块级作用域

let实际上为JS在全局作用域和函数作用域的基础上新增了块级作用域。同一个变量名可以在不同的层级重新声明,每一层都是一个单独的作用域,互不影响。

1
2
3
4
5
6
7
function f() {
let n = 1;
if (true) {
let n = 2;
}
console.log(n); // 1
}

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明,如if(true){},try{}catch(e){}。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。 ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

注:考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

const

  1. 只读的常量,声明后值不变
  2. 声明变量时必须初始化,声明不赋值会报错
  3. 变量不提升,先声明后使用

const的本质是变量指向的内存地址所保存的数据不得改动,对于复合类型的数据如对象和数组,变量指向的内存地址保存的是指向实际数据的指针,const保证这个指针不变,所以给对象添加新属性并不会报错。

解构赋值

数组的解构赋值

类似模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

1
2
3
4
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

如果等号右边不是可遍历的结构,那么将会报错。

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 接口。 事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

例如下面这个Generator函数:

1
2
3
4
5
6
7
8
9
10
11
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

默认值:只有解构被赋予的值严格等于undefined才生效,并且如果能直接取到非undefined的值就不会去求默认值。

1
2
3
4
5
let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

对象的解构赋值

与数组的解构赋值类似

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

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

如果变量名与属性名不一致用以下写法:

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

对象的解构赋值是下面形式的简写:

1
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

对象解构赋值的内部机制是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

1
2
3
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

在上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

函数参数的解构赋值

1
2
3
4
5
6
7
8
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]

字符串

字符串遍历器接口

1
2
3
for (let char of 'hello') {
console.log(char)
}

模板字符串

1
2
3
4
5
info = {
name: 'nancy',
desc: 'hahaha'
}
let str = `Hi, i'm ${info.name}! ${info.desc}~`;
1
2
3
4
5
6
7
//这里的空格和缩进都会保留
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);

模板字符串之中还能调用函数

1
2
3
4
5
function fn() {
return "Hello World";
}

`${fn()}` // Hello World

includes(), startsWith(), endsWith()

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

函数的扩展

箭头函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var f = v => v;
// 等同于
var f = function (v) {
return v;
};

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};

[1,2,3].map(function (x) {
return x * x;
});
// 等同于
[1,2,3].map(x => x * x);

注意点:

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象;this会绑定定义时所在的作用域,而不是运行时所在的作用域。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function foo() {
    return () => {
    return () => {
    return () => {
    console.log('id:', this.id);
    };
    };
    };
    }

    var f = foo.call({id: 1});

    var t1 = f.call({id: 2})()(); // id: 1
    var t2 = f().call({id: 3})(); // id: 1
    var t3 = f()().call({id: 4}); // id: 1

    上面代码之中,只有一个this,就是函数foothis,所以t1t2t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this

  2. 不可以当作构造函数,不可以使用new命令 。

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

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

箭头函数不适用的场景:

  1. 不适于定义对象的方法:

    1
    2
    3
    4
    5
    6
    const obj = {
    count: 0,
    add: () => {
    this.count++;
    }
    }

    这里的obj.add()使用了箭头函数,在调用obj.add()的时候this会指向全局对象,因为对象不构成单独的作用域,导致add方法定义时的作用域就是全局作用域,所以这里不能使用箭头函数。

  2. 不适于需要动态this的场合:

    1
    2
    3
    4
    let button = document.getElementById('press');
    button.addEventListener('click', () => {
    this.classList.toggle('on');
    });

    上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

rest参数

形为:...变量名

1
const sortNumbers = (...numbers) => numbers.sort();

注意rest 参数之后不能再有其他参数(即只能是最后一个参数),否则报错。

1
2
3
4
// 报错
function f(a, ...b, c) {
// ...
}

数组的扩展

扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

1
2
3
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象。

1
2
3
4
5
6
7
8
9
10
11
12
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

任何有length属性的对象,都可以通过Array.from方法转为数组

1
2
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

1
2
3
4
5
6
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

Array.of()

Array.of方法用于将一组值,转换为数组。 Array.of总是返回参数值组成的数组;如果没有参数,就返回一个空数组。

1
2
3
Array.of() // []
Array.of(1) // [1]
Array.of(undefined) // [undefined]

find() 和 findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

1
2
3
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

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

includes()

includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。

对象的扩展

属性简洁表示

ES6 允许直接写入变量和函数,作为对象的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

const o = {
method() {
return "Hello!";
}
};

// 等同于
const o = {
method: function() {
return "Hello!";
}
};

CommonJS 模块输出一组变量,就非常合适使用简洁写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
let ms = {};

function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}

module.exports = { getItem, setItem, clear };

super 关键字

指向当前对象的原型对象。 super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
const proto = {
foo: 'hello'
};

const obj = {
foo: 'world',
find() {
return super.foo;
}
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。 Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

1
2
3
4
5
6
7
const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

注意点:

  1. Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

    1
    2
    3
    4
    5
    const obj1 = {a: {b: 1}};
    const obj2 = Object.assign({}, obj1);

    obj1.a.b = 2;
    obj2.a.b // 2
  2. 对于同名属性,Object.assign的处理方法是替换而不是添加。

    1
    2
    3
    4
    const target = { a: { b: 'c', d: 'e' } }
    const source = { a: { b: 'hello' } }
    Object.assign(target, source)
    // { a: { b: 'hello' } }
  3. Object.assign可以用来处理数组,但是会把数组视为对象。

    1
    2
    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]

    上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1

Object.keys(),Object.values(),Object.entries()

Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

Object.entries()方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

Symbol

Symbol(),getOwnPropertySymbols()

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol 值通过Symbol函数生成。 凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

Symbol 作为属性名,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。

Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

1
2
3
4
5
6
7
8
9
10
11
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]

另一个新的 API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

1
2
3
4
5
6
7
8
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};

Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]

Symbol.for(),Symbol.keyFor()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

1
2
3
4
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

1
2
3
4
5
Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key

1
2
3
4
5
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Set和Map

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():清除所有成员,没有返回值。
  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

Map的基本属性和方法

  • size 属性
  • Map.prototype.set(key, value):设置键名key对应的键值为value
  • Map.prototype.get(key):读取key对应的键值
  • Map.prototype.has(value):返回一个布尔值
  • Map.prototype.delete(key) :删除某个键,返回true;如果删除失败,返回false
  • Map.prototype.clear():清除所有成员,没有返回值。
  • 遍历方法类似Set

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。 但是WeakSet 的成员只能是对象,而不能是其他类型的值,并且WeakSet 中的对象都是弱引用,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。 WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名,且WeakMap的键名所指向的对象,不计入垃圾回收机制(类似WeakSet)。

WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

注:WeakMap 弱引用的只是键名,而不是键值,键值依然是正常引用。只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。

1
2
3
4
5
6
7
8
9
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();

myWeakmap.set(myElement, {timesClicked: 0});

myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);

上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。

Promise

1
2
3
4
5
6
7
8
9
const promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

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

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

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

then方法返回的是一个新的Promise实例。因此可以采用链式写法,即then方法后面再调用另一个then方法,这样就可以依次调用回调函数 。

1
2
3
4
5
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});

异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。then方法指定的回调函数在运行中抛出的错误也会被catch方法捕获。

1
2
3
4
5
6
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.all([p1, p2, p3]);

p的状态由p1、p2、p3决定,分成两种情况:

  1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.resolve 可以将现有对象转为 Promise 对象。

1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

如果不知道或者不想区分函数f是同步函数还是异步操作,但是想用 Promise 来处理它,即让同步函数同步执行,异步函数异步执行。

1
2
3
4
5
6
7
8
const f = () => console.log('now');
(async () => f())()
.then(...)
.catch(...)

//或者
const f = () => console.log('now');
Promise.try(f);

Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

Iterator遍历器

Iterator可以使数据按次序排列,并且主要为for…of循环提供接口。主要包括:Array,String,Map,Set,Object(大部分需要手动部署)。

1
2
3
4
5
var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

默认的 Iterator 接口部署在数据结构的Symbol.iterator属性 ,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的” 。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};

let it = obj[Symbol.iterator]()
it.next() //{value: 1, done: true}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};

让for…of可以遍历对象的办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//方法一
for (var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}

//方法二:使用Generator包装
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}

for (let [key, value] of entries(obj)) {
console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

Generator

Generator 函数是一个状态机,封装了多个内部状态,并返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};

for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6

与Iterator接口的关系:

1
2
3
4
5
6
7
8
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]

利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}

// foo 3
// bar 7

Generator 函数和for...of循环,实现斐波那契数列的例子:

1
2
3
4
5
6
7
8
9
10
11
12
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}

for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}

yield*表达式用来在一个 Generator 函数里面执行另一个 Generator 函数。

1
2
3
4
5
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}

async

基本用法

async 是 Generator 函数的语法糖。 async函数将 Generator 函数的星号(*)替换成async,将yield替换成await

async函数自带执行器 ;其返回值是 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
25
26
27
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

注意点:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    async function myFunction() {
    try {
    await somethingThatReturnsAPromise();
    } catch (err) {
    console.log(err);
    }
    }

    // 另一种写法
    async function myFunction() {
    await somethingThatReturnsAPromise()
    .catch(function (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 函数可以保留运行堆栈。

实现原理

将 Generator 函数和自动执行器,包装在一个函数里。

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

// 等同于

function fn(args) {
return spawn(function* () { //spawn函数是自动执行器
// ...
});
}

spawn函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function spawn(genF) {  //传入generator函数
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}

与其他异步方法的比较

假定某个 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用来保存上一个动画的返回值
let ret = null;

// 新建一个空的Promise
let p = Promise.resolve();

// 使用then方法,添加所有动画
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}

// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});

}

Generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function chainAnimationsGenerator(elem, animations) {

return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});

}

async:

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

Async 函数的实现最简洁,最符合语义,容易理解,几乎没有语义不相关的代码,并且不需要自己提供自动执行器。

按顺序完成异步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//继发
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}

//并发
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});

// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}

虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。

CATALOG
  1. 1. ES6常用新特性整理
    1. 1.1. let
      1. 1.1.1. let与var的不同
      2. 1.1.2. 块级作用域
    2. 1.2. const
    3. 1.3. 解构赋值
      1. 1.3.1. 数组的解构赋值
      2. 1.3.2. 对象的解构赋值
      3. 1.3.3. 函数参数的解构赋值
    4. 1.4. 字符串
      1. 1.4.1. 字符串遍历器接口
      2. 1.4.2. 模板字符串
      3. 1.4.3. includes(), startsWith(), endsWith()
    5. 1.5. 函数的扩展
      1. 1.5.1. 箭头函数
      2. 1.5.2. rest参数
    6. 1.6. 数组的扩展
      1. 1.6.1. 扩展运算符
      2. 1.6.2. Array.from()
      3. 1.6.3. Array.of()
      4. 1.6.4. find() 和 findIndex()
      5. 1.6.5. includes()
    7. 1.7. 对象的扩展
      1. 1.7.1. 属性简洁表示
      2. 1.7.2. super 关键字
      3. 1.7.3. Object.assign()
      4. 1.7.4. Object.keys(),Object.values(),Object.entries()
    8. 1.8. Symbol
      1. 1.8.1. Symbol(),getOwnPropertySymbols()
      2. 1.8.2. Symbol.for(),Symbol.keyFor()
    9. 1.9. Set和Map
      1. 1.9.1. Set的基本属性和方法
      2. 1.9.2. Map的基本属性和方法
      3. 1.9.3. WeakSet
      4. 1.9.4. WeakMap
    10. 1.10. Promise
    11. 1.11. Iterator遍历器
    12. 1.12. Generator
    13. 1.13. async
      1. 1.13.1. 基本用法
      2. 1.13.2. 实现原理
      3. 1.13.3. 与其他异步方法的比较
      4. 1.13.4. 按顺序完成异步操作