简单介绍

分享内容可分为两块:优雅的异步编程JavaScript的类

GeneratorES6 的一种异步编程的方案, ES2017 引入了 async 函数,它是 Generator 函数的语法糖,它让异步操作更加方便。

JavaScriptES6 之前一直没有类的概念,生成实例的方法就是通过构造函数。但是 类(Class) 只是一个语法糖,它实际上是构造函数和对象原型写法的优化。

????Generator 函数的语法

最基本的使用

Generator 可以理解为一个状态机,它封装了多个内部状态
执行 Generator 会得到一个遍历器对象,所以它也是遍历器生成函数。

定义一个 Generator 函数

了解一下 Generator 函数的特征

function* helloWorld() {    yield 'hello'    yield 'world'    return 'ending'}

一是 * : 在关键词 function 和 函数名之间有一个 *
二是 yield: 函数体内部使用yield表达式,定义不同的状态

调用

Generator 函数的调用和普通函数一样,比如 helloWrold(),不同的是,调用 Generator 函数并不会立即执行,它会返回一个遍历器对象,然后通过它的next方法来获取内部状态

通过不断的调用遍历器的 next 方法,来获取下一个状态,每次调用 next 方法,都会执行到下一个 yield表达式

hello.next()    // {value: "hello", done: false}hello.next()    // {value: "world", done: false}hello.next()    // {value: "ending", done: true}hello.next()    // {value: undefined, done: true}

返回一个有 valuedone 两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

yield 表达式

yield 表达式就是: yield + 运算表达式

yield 是暂停标志

next方法的运行逻辑

1.遇到 yield表达式 就暂停后面的操作,将表达式的结果作为返回对象的value值

2.下一次调用next方法,继续执行,直到遇到下一个yield表达式

3.如果没有yield表达式,就一直运行到函数结束,如果有return,则把return的值 作为返回对象的value值,如果没有return,则把undefined 作为返回对象的value值

有个小问题,假如调用next方法,请问此时是否会执行赋值操作?

let a = yield 1

答案是不会,因为执行到yield表达式就暂停,赋值操作会在下一次执行next方法是执行。而next传入的参数也就是yield表达式的返回值。这个一会详细说1️。

yield和return

相似的地方:

两者都能返回表达式的值

不同的地方:

1.yield是暂停执行,而return表示函数运行结束

2.一个函数中只能执行一次return语句,而yield可以执行多次。所以正常函数调用时只能返回一个值,而Generator函数可以返回多个值

循环中使用yield

一维数组遍历

function* func(arr) {    for (let i = 0; i < arr.length; i++) {        yield arr[i]    }}let arr = [1, 3, 6, 7]for (var f of func(arr)) {    console.log(f)}// 1, 3, 6, 7

多维数组遍历

function* func(arr) {    for (let i = 0; i < arr.length; i++) {        if (typeof arr[i] === 'number') {            yield arr[i]        } else {            yield* test(arr[i])        }    }}let arr = [1, [[3, 6], 9], 7]let t = func(arr)[...t]   // 可以使用 ... 代替 for of 遍历 遍历器// 1, 3, 6, 9, 7

这个函数里面还使用了 yield* 表达式,后面会详细介绍2️

放在别的表达式中,一般都要用圆括号,为什么?
function* demo() {    // console.log('Hello' + yield); // SyntaxError    console.log('Hello' + (yield 123)); // OK}

运算符优先级

和Iterator的关系

任意一个对象的 Symbol.iterator 属性,就是该对象的遍历器生成函数

是不是可以把 Generator 函数赋值给普通对象的 Symbol.iterator 属性,从而使对象具有 Iterator 接口。如果可以,则意味着普通对象也可以使用 for..of 或者 扩展运算符 等方法。下面会详细说3️

function* func() {    yield 1    yield 2    yield 3}let myIterable = {}myIterable[Symbol.iterator] = func  // myIterable为遍历器对象[...myIterable]// [1, 2, 3]

遍历器对象的 Symbol.iterator 执行后,会返回自身。

function* func() {    yield 1    yield 2    yield 3}let f = func()f[Symbol.iterator]() === f  // 遍历器对象的Symbol.iterator属性是遍历器生成函数,调用这个函数会返回它自身。// true

next方法的参数(上文的1️)

yield表达式 本身没有返回值。

这里要区分一下

1.yield表达式的返回值
2.调用next方法的返回值

调用遍历器对象next方法的返回值:
调用next方法,会返回一个对象,对象有 valuedone 属性,这里的 value 是 yield 关键字之后语句的执行结果

而在遍历器对象的内部 yield表达式 的值,是next方法传入的参数。

function* func() {    let first = yield 1;    let second = yield first + 2;    yield second + 3;}let f = func()f.next()    // {value: 1, done: false}f.next(5)   // {value: 7, done: false}f.next(2)   // {value: 5, done: false}

总结一下:
通过next方法传入参数,可以向函数体内部注入值

因为next方法的参数表示上一个yield表达式的返回值,所以第一个next不需要传递参数,第二个next传的参数是第一个yield表达式的返回值。

从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数

for...of循环

for...of循环可以自动遍历 Generator 函数运行时生成的 Iterator对象,遍历会显示yield表达式的值,<u>不包含return</u>,return后面会说4️。

利用for...of循环,遍历对象(上文的3️)

原生的 JavaScript 对象没有遍历接口,所以要通过Generator为它加上这个接口

第一种方法是以普通对象为参数,借用Generator函数,实例化一个新的遍历器对象

function* objectEntries(obj) {    let propKeys = Reflect.ownKeys(obj);    for (let propKey of propKeys) {        yield [propKey, obj[propKey]]    }}let jane = { first: 'Jane', last: 'Doe' }for (let [key, value] of objectEntries(jane)) {    console.log(`${key}: ${value}`)}// first: Jane// last: Doe

第二种方式是将Generator函数添加到普通对象的Symbol.iterator属性上面

function* objectEntries() {  let propKeys = Object.keys(this); // this后面会详细说5️  // 这里的this是 jane,原因是:for...of遍历 jane,实际上是,jane先调用Symbol.iterator,返回遍历器对象,然后在被for...of遍历  // jane先调用Symbol.iterator 所以this是jane  console.log(this)     // {Symbol(Symbol.iterator): ƒ}  for (let propKey of propKeys) {    yield [propKey, this[propKey]];  }}let jane = { first: 'Jane', last: 'Doe' };jane[Symbol.iterator] = objectEntries;for (let [key, value] of jane) {  console.log(`${key}: ${value}`);}// first: Jane// last: Doe

所以,其实for...of循环时,会默认会调用目标的 iterator 接口,从而得到一个遍历器对象。

除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,也都是 iterator 接口。

function* numbers () {  yield 1  yield 2  return 3  yield 4}// 扩展运算符[...numbers()] // [1, 2]// Array.from 方法Array.from(numbers()) // [1, 2]// 解构赋值let [x, y] = numbers();x // 1y // 2// for...of 循环for (let n of numbers()) {  console.log(n)}// 1// 2let obj = {}obj[Symbol.iterator] = numbers[...obj[Symbol.iterator]()] // [1, 2][...obj] // [1, 2]Array.from(obj[Symbol.iterator]())  // [1, 2]Array.from(obj) // [1, 2]let [x, y] = obj[Symbol.iterator]()    // [1, 2]let [x, y] = obj    // [1, 2]for (let n of obj[Symbol.iterator]()) {  console.log(n)}// 1// 2for (let n of obj) {  console.log(n)}// 1// 2

总结一下: for...of循环,扩展运算符(...)、解构赋值和Array.from方法默认都调用参数的 Symbol.iterator 接口,得到遍历器对象并将它作为新的参数。

错误捕获

Generator.prototype.throw()

遍历器对象,都有一个throw方法,能抛出错误,然后在 Generator 函数体内捕获。

内部或外部捕获错误

function* func() {    yield 1    try {        yield    } catch(e) {        console.log('内部捕获', e)    }}var f = func()f.next()    // {value: 1, done: false}f.next()    // {value: undefined, done: false}try {    f.throw(new Error('a'));    f.throw(new Error('b'));} catch (e) {    console.log('外部捕获', e);}// 内部捕获 a// 外部捕获 b

1.遍历器对象f抛出错误,在遍历器中可以被 try...catch 捕获。

要注意,接收 throw方法抛出错误的 yield表达式要在try catch语句中

2.区分遍历器对象的throw方法和全局throw方法

前者可以在Generator中捕获
后者只能在全局捕获

3.如果 Generator 函数内部没有try...catch,throw方法抛出的错误,就会被外部的try...catch捕获。

4.如果 Generator 函数内部和外部,都没有try...catch,那么将报错中断执行。

5.throw方法被捕获以后,会附带执行下一条yield表达式。

相当于执行一次next方法。
只要 Generator 函数内部有try...catch,遍历器的throw方法抛出的错误,不影响下一次遍历。

throw可以看做next方法传入了一个错误。所以要考虑 try...catch 代码捕获哪个 yield表达式,调用第几次next方法后,再执行throw会被捕获?

例子

如果遍历器内部抛出错误
1.内部没有错误捕获处理

function* foo() {  var x = yield 3;  var y = x.toUpperCase();  yield y;  yield 10}var it = foo();it.next(); // { value:3, done:false }try {  it.next(42);} catch (err) {  console.log(err, 'err 外部');}// TypeError: x.toUpperCase is not a function   "err 外部"it.next()// {value: undefined, done: true}

如果 Generator 执行过程中抛出错误,而且没有被内部捕获,就不会再执行了。
之后调用next方法,将返回 {value: undefined, done: true},也就是说,这个 Generator 已经运行结束了。

2.内部有错误捕获处理

function* foo() {    var x = yield 3;    try {          var y = x.toUpperCase();    } catch(e) {        console.log(e, 'err 内部')    }    yield y;    yield 10}var it = foo();it.next()   // {value: 3, done: false}try {  it.next(42);} catch (err) {  console.log(err, 'err 外部');}// TypeError: x.toUpperCase is not a function  "err 内部"// {value: undefined, done: false}it.next()// {value: 10, done: false}

return(上文的4️)

Generator.prototype.return()

遍历器对象,有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

function* func() {  yield 1;  yield 2;  yield 3;}var f = func();f.next()        // { value: 1, done: false }f.return('foo') // { value: "foo", done: true }f.next()        // { value: undefined, done: true }

如果调用return时不传参数,返回值的value属性为undefined。

next()、throw()、return() 的共同点

三个方法都可以让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

function* func() {    let a = yield}let f = fun()

next()是将yield表达式替换成一个值。

f.next(1)// 相当于 let a = 1

throw()是将yield表达式替换成一个throw语句。

f.throw(new Error('error'))// 相当于 let a = throw(new Error('error')) 当然这个肯定会执行失败

return()是将yield表达式替换成一个return语句

f.return(1)// 相当于 let a = return 1

yield* 表达式(上文的2️)

如果在 Generator 函数中,在调用另一个 Generator 函数,是怎么样的情况?

yield* 后面跟遍历器对象,也可以是 有Symbol.iterator 接口的原生遍历器,比如数组,字符串

function* foo () {    yield 3    yield 4}function* bar () {    yield 1    yield 2    yield* foo()}// 其中 yield* foo() // 等同于 // for (let v of foo()) { //   yield v// }let res = bar()[...a]// [1, 2, 3, 4]

所以在 yield 后面加上星号,表明它返回的是一个遍历器对象。这就是 yield* 表达式。

yield* 表达式 等同于一个for...of循环

当 yield* 后面的Generator函数中有return语句时,可以通过赋值获取return的值,类似于 var value = yield* iterator

function* func() {    let r = yield* foo()    yield r}function* foo() {    yield 1    return 2}let f = func()f.next()    // {value: 1, done: false}f.next()    // {value: 2, done: false}f.next()    // {value: undefined, done: true}

如果yield* 后面跟原生的遍历器

数组

function* func() {    yield* [1, 2, 3, 4]}f = func()f.next()    {value: 1, done: false}// ...

字符串

function* func() {    yield* "hello"}f = func()f.next()// {value: "h", done: false}

yield* 的小应用

function* func(val) {    if (Array.isArray(val)) {        for (let i = 0; i < val.length; i++) {            yield* func(val[i])        }    } else {        yield val    }}let arr = ['a', ['b', 'c'], ['d', 'e'] ][...func(arr)]// ["a", "b", "c", "d", "e"]

作为对象属性的 Generator 函数

完整形式

let obj = {    func: function* () {}}

简写形式

let obj = {    * func () {}}

属性前面有一个星号,表示这个属性是一个 Generator 函数。

Generator 函数的this(上文的5️)

遍历器对象 是 Generator函数的 实例,这个实例继承 Generator函数 原型上的方法。

但是 Generator函数 不是构造函数,它返回的是遍历器对象,而不是this对象

function* func() {    this.a = 1    yield this.a}let f = func()f.next() // 1f.a  // undefined
function* F() {  yield this.x = 2;  yield this.y = 3;}new F()// TypeError: F is not a constructor

所以不能用new调用,生成的实例也获取不到this的属性

可以在调用时把 Generator函数 的原型设置为它的this,它的实例就能获取到this的值了。相当于是直接把属性加在原型上了

function* func() {    this.a = 1    yield this.b = 2    yield this.c = 3}let f = func.call(func.prototype)f.a       // undefinedf.next()    // {value: 2, done: false}f.a       // 1

Generator 应用

异步操作的同步化

Generator函数可以暂停执行,所以可以把异步操作写在yield表达式中,等到异步完成的时候,再调用next方法时往后执行。

最常见的应用场景就是ajax获取数据。
示例: vue-app-train/Generator.vue demo1()

流程控制

数组steps封装了一个任务的多个步骤

function func1() {    console.log(1)}function func2() {    console.log(2)}function func3() {    console.log(3)}let steps = [func1, func2, func3]function* iterateSteps(steps){  for (var i=0; i< steps.length; i++){    var step = steps[i];    yield step();  }}for (var step of iterateSteps(steps)){}// 1 2 3

将项目分解成多个依次执行的任务

Iterator 接口

没有Iterator接口的可以通过 Generator 函数实现,比如普通对象

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

已经有了 iterator 接口,也可以通过 Generator 函数定义不一样的行为

function* makeSimpleGenerator(array){    var nextIndex = 0;    while(nextIndex < array.length){        yield [nextIndex, array[nextIndex++]];    }}let arr = [1, 3, 6, 7]var gen = makeSimpleGenerator(arr);for (let [key, value] of gen) {  console.log(key, value);}// 0 1// 1 3// 2 6// 3 7

Generator 函数的异步应用

传统的异步编程方案

1.回调函数
2.事件监听
3.发布/订阅
4.Promise 对象
Javascript异步编程的4种方法

概念

回调函数

JS对异步编程的实现,就是回调函数。
可以把异步理解为,一个任务分为不连续的两段,在第一段完成时,再开始第二段

以读取文件内容为例

fs.readFile(path1, function (err, data) {    if (err) {        console.log(err)        return    }    console.log(data.toString(), 'data1')})

raadFile 是node.js 中 fs模块异步读取文件内容的方法,传入的函数就是回调函数,这个函数在读取完成后才会执行。

了解:
为什么 Node 回调函数的第一个参数,必须是错误对象err?

因为第一段执行完成后,任务所在的上下文环境就已经结束了,在这以后抛出的错误,只能当作参数,传入第二段。

Promise

假设有一个需求:在读取A文件之后,在读取B文件

fs.readFile(path1, function (err, data) {    if (err) {        console.log(err)        return    }    console.log(data.toString(), 'data1')    fs.readFile(path2, function (err, data) {        if (err) {            console.log(err)            return        }        console.log(data.toString(), 'data2')    })})

异步操作的嵌套,代码臃肿,耦合严重,简直就是噩梦,嗯,所以称为回调地狱

Promise来了

Promise不是新的语法,只是一种新的写法,就是把回调的形式写成链式调用的形式

示例:在 train/node/server/libs/readFile.js

Promise的写法是回调函数的改进,但Promise依然面临:代码冗余,语义不清的问题。

Generator函数 理解

思考几个问题:
1.执行多个Generator函数,分别调用实例的next方法后,问,此时这些Generator是什么状态? 正在执行? 还是已经执行完成并退出上下文?

答案是:没有退出,也没有继续执行,而是处于暂停态。通过next方法可以继续执行某个Generator函数,但是多个Generator函数中,只会有一个处在运行状态。

由此可知:Generator执行后不会像普通函数一样被释放回收

2.Generator函数和传统的函数不同在哪?
传统函数只有一个调用栈,且在内部调用的子函数执行完成后,才会结束执行外层函数。
而Generator函数有自己的调用栈,即使它没有执行完毕,也可以执行另外的函数。

JS只有一个调用栈。但是有了 Generator 函数,每个 Generator 函数都可以保持自己的调用栈

分析一个例子

train/node/server/libs/generatorReadFile.js 中,asyncReadFile 是一个Generator函数,调用它会产生一个新的调用栈,在执行完iterator.next(),就是执行yield表达式之后。asyncReadFile 函数处于暂停态,程序可以继续执行其他的逻辑,等到异步行为成功后,再通过next方法重新恢复 asyncReadFile 的执行。

Generator 函数的数据交换和错误处理

Generator 函数可以暂停执行和恢复执行,还有两个重要的特性:函数体内外的数据交换和错误处理机制

数据交换是通过 next方法 和 yield。

异步任务的封装

vue-app-train/demo/Generator.vue demo2()

Thunk 函数

Thunk函数关注的是函数参数的求值问题。

1) 为什么要了解Thunk函数?

它是实现Generator函数的自动执行的一种方式

2) 现在已经有相关的模块,可以很方便的实现Generator函数的自动执行,所以只是简单看一下怎么实现的?

JavaScript 的 Thunk 函数

示例:train/node/server/libs/thunk.js

在JS中,Thunk函数是说,将多参数函数替换成一个只接受回调函数作为参数的单参数函数。

写成thunk函数有什么用?后面会说

Thunk函数转换器

不可能针对每个函数都写一个Thunk函数,所以有Thunk函数转换器

const Thunk = function(fn) {    return function (...args) {        return function (callback) {            return fn.call(this, ...args, callback);        }    };};

示例:train/node/server/libs/thunk.js

Thunkify 模块

生产环境的转换器的模块
示例:train/node/server/libs/thunkify-readFile.js

那么Thunkify和刚才写的Thunk函数最重要的区别是什么?
多了一个检查机制

确保回调函数只运行一次

示例:train/node/server/libs/thunk-thunkify.js

Generator 函数的自动执行

Thunk函数和Promise 都可以实现 Generator 函数的自动执行

Thunk + Generator 示例:train/node/server/libs/thunkify-generator.js

Promise + Generator 示例:train/node/server/libs/promise-generator.js

自动控制 Generator 函数的流程的关键是,每一次执行完成,都能调用next方法,返还执行权

co 模块

co 模块用于 Generator 函数的自动执行。

co + generator 示例:train/node/server/libs/co-generator.js

co 可以自动执行 Generator 函数的原因?

co 就是将Thunk函数和Promise两种自动执行方式包装成一个模块。使用时必须保证:Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。

async 函数

async 是 Generator 函数的语法糖

async 与 generator 对比

用 async 实现读取文件

async示例:train/node/server/libs/asyncReaedFile.js

其实,async 函数就是将 Generator 函数的星号 * 替换成 async,将 yield 替换成 await

具体改进的地方

1) 内置执行器

之前介绍了,Generator自动执行,以来执行器,所以有co模块,async 函数自带执行器。

2) 语义更好

async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果

3) 适用性更好

第一条说明了generator需要执行器,使用执行器的条件是,yield命令后只能跟Promise对象或者Thunk函数
await命令后面可以是Promise对象,或者是原始类型的值,但会转成立即resolved 的 Promise 对象

4) 返回值是Promise,可以使用then方法指定下一步的操作

基础用法

延时执行

示例 vue-app-train/Async.vue timeout()
timeout方法返回一个Promise对象

示例 vue-app-train/Async.vue asyncTimeout()
asyncTimeout 是一个async函数,它也是返回一个Promise对象。

如果没有指定返回值,则会把undefined 转成一个立即 resolved 的 Promise 对象,这时then方法回调函数的参数就是undefined

多种形式

// 函数声明async function foo() {}// 函数表达式const foo = async function () {};// 对象的方法let obj = { async foo() {} };obj.foo().then(...)// 箭头函数const foo = async () => {};

语法

返回Promise对象

async函数内部return语句返回的值,会成为then方法回调函数的参数。

还是刚才的示例:vue-app-train/Async.vue demo1()

如果async函数内部抛出错误,则返回的Promise对象是reject状态,错误对象可以被catch捕获。

示例:vue-app-train/Async.vue demo2()

什么时候会执行then方法指定的回调函数?

除过遇到return语句或者抛出错误,async函数返回的 Promise 对象,必须要等到所有的await命令执行完成。

示例 train/node/server/libs/asyncReaedFile.js

await命令和错误处理

  1. 如果await后面是Promise对象,则返回对象的结果,如果是原始类型的值,则返回对应的值。

示例 vue-app-train/Async.vue demo3()

  1. 如果await后面是一个定义 then 方法的对象,await 将其等同于Promise对象。

示例 vue-app-train/Async.vue demo4()

  1. await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

示例 vue-app-train/Async.vue demo2()

  1. 任意一个await 后面的Promise对象状态变为reject,则async函数会中断执行。

示例 vue-app-train/Async.vue demo2()

解决方法是:
1.可以将await表达式放在try...catch结构里面 示例 vue-app-train/Async.vue demo5()
2.在await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。 示例 vue-app-train/Async.vue demo6()

注意点

  1. await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。已经介绍过了,有两种写法
  2. 多个await命令后面的异步操作,如果没有依赖关系,可以同时触发。 示例 train/node/server/libs/asyncReaedFileOpt.js readFileAsyncFunc()
  3. await 在普通函数中会报错
  4. 在forEach方法中和for循环中使用async,await。示例 train/node/server/libs/asyncReadFileOpt.js dbFuc()
  5. async 函数可以保留运行堆栈
一般的异步任务出错时,上下文环境可能已经运行结束了,所以异步任务的报错信息一般不包括上下文。
function f1 () {    Promise.resolve(1).then((res) => {        console.log(a)    })}async function f2 () {    await Promise.resolve(1).then((res) => {        console.log(a)    })}function f3 () {    let b = function () {        console.log(a)    }    b()}f1()    // at <anonymous>:3:15f2()    // at <anonymous>:3:15        // at async f2 (<anonymous>:2:2)f3()    // at b (<anonymous>:3:15)        // at f3 (<anonymous>:6:2)        // at <anonymous>:1:1

async 函数的实现原理

实现原理就是 Generator 函数和自动执行器

异步遍历器 和 同步遍历器

之前介绍的遍历器都是同步遍历器,同步遍历器有什么特点呢?

它的next方法必须是同步的,调用next方法必须同步得到包含 value 和 done 两个属性的对象。

也就是,调用next方法依然是同步返回 value 和 done 两个属性,只不过,value是 Thunk 函数或者 Promise 对象

在 ES2018 引入了异步遍历器”(Async Iterator)

异步 Generator 函数

async function* gen() {    yield 'hello';}const genObj = gen();genObj.next().then(x => console.log(x));

这就是异步的 Generator 函数,就是async函数 与 Generator 函数的结合

只要是一个 异步 Generator 函数,它生成的遍历器对象next方法返回的就是一个 Promise 对象

异步遍历的接口

同步遍历器的接口在 Symbol.iterator 属性上,同样,异步遍历的接口在 Symbol.asyncIterator 属性上。

假定 asyncIterator 是一个异步遍历器对象,它调用next方法,然后会返回一个Promise对象,调用then方法,回调函数的参数,就是 value 和 done 属性。

const asyncIterable = {};asyncIterable[Symbol.asyncIterator] = async function*() {    yield "hello";    yield "async";    yield "iteration!";};let asyncIterator = asyncIterable[Symbol.asyncIterator]()asyncIterator.next().then((res) => {console.log(res)})// {value: "hello", done: false}for await (x of asyncIterable) {    console.log(x);}// hello// async// iteration

使用async await 把异步遍历器的写法写成同步

const asyncIterable = new Object();asyncIterable[Symbol.asyncIterator] = async function*() {    yield "hello";    yield "async";    yield "iteration!";};let asyncIterator = asyncIterable[Symbol.asyncIterator]()console.log(await asyncIterator.next());// {value: "hello", done: false}console.log(await asyncIterator.next());// {value: "async", done: false}console.log(await asyncIterator.next());// {value: "iteration!", done: false}

连续调用next方法时,会自动按顺序执行下去
同时调用多个next方法,放在Promise.all方法里面

// ...let [{value: v1}, {value: v2}] = await Promise.all([  asyncIterator.next(), asyncIterator.next()]);console.log(v1, v2); // hello async

for await...of

for await...of循环,遍历异步的 Iterator 接口。

let asyncIterable = {}function timeout(v) {    return new Promise((res) => {        setTimeout(() => {            res(v)        }, 1000)    })}asyncIterable[Symbol.asyncIterator] = async function*() {    yield timeout("hello");    yield timeout("async");    yield timeout("iteration");};try {    for await (x of asyncIterable) {        console.log(x)    }} catch (e) {}// hello// async// iteration

Promise对象状态是reject时,for await...of会报错,所以用try...catch捕捉

异步 Generator 总结

1.对比同步的Generator函数
同步遍历器对象next方法 返回的 value属性 必须是一个Promise对象或者Thunk函数,但是在有了异步遍历器之后,可以直接返回异步的结果。

2.next方法返回一个Promise对象,是个什么概念?
next方法返回一个Promise对象,可以理解为将原来的返回结果,包装为Promise对象,在Promise对象内部,把{value, done} 当做回调函数的参数返回。

3.同步遍历器和异步遍历器执行顺序对比
同步遍历器执行顺序:

function* syncGenerator() {    console.log('Start');    yield timeout(1); // (B)    console.log('Done');}function timeout(v) {    return new Promise((res) => {        setTimeout(() => {            res(v)        }, 1000)    })}let syncGen = syncGenerator()let value = syncGen.next().valuevalue.then((res) => {    console.log(res)})// {value: 1, done: false}

同步遍历器对象,调用next方法直接返回一个普通对象,value属性是一个Promise对象,就是 yield 之后的timeout(1)。

执行顺序:
1.首先执行Generator函数,得到遍历器对象
2.调用遍历器对象的next方法
3.开始执行Generator函数,打印Start
4.执行到 yield 时,会取到后面timeout(1)的执行结果
5.syncGen.next() 返回一个普通对象
6.调用普通对象中value属性的then方法,then方法指定的回调函数的参数res就是timeout 中res()的参数

异步遍历器执行顺序:

async function* asyncGenerator() {    console.log('Start');    yield await timeout(1); // (A)    console.log('Done');}function timeout(v) {    return new Promise((res) => {        setTimeout(() => {            res(v)        }, 1000)    })}let asyncGen = asyncGenerator()asyncGen.next().then((v) => {    console.log(v)})// {value: 1, done: false}// 上面 A 处类似于(function () {    return new Promise((resolve, reject) => {        timeout(1).then((res) => {            resolve(res)        }); // (B)    })})()

异步遍历器对象,调用next方法直接返回一个Promise对象,then方法回调的参数是一个包含value和done的对象,value属性是 yield 之后 await timeout(1) 返回的值

执行顺序:
1.首先执行异步Generator函数,得到异步遍历器对象
2.调用异步遍历器对象的next方法,会直接返回一个Promise对象
3.此时asyncGenerator函数开始执行,打印Start
4.执行到yield,暂停执行Generator。等待await timeout(1)返回结果。
5.await timeout(1) 变为完成状态,并返回值。
6.yield 命令取到这个值,第二步得到的Promise对象变为完成状态。
7.开始执行 (第二步的Promise对象)then方法指定的回调函数,回调函数的参数是一个对象:{value, done},value的值是yield命令后面的那个表达式的值。

异步 Generator 函数内部,能够同时使用await和yield命令。可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。

异步 Generator 函数的执行器

async 函数自带执行器,它的返回值是 Promise

异步遍历器对象也需要调用next方法得到执行权。可以通过for await...of执行,或者使用异步 Generator 函数的执行器

async function takeAsync(asyncIterable, count = Infinity) {    const result = [];    const iterator = asyncIterable[Symbol.asyncIterator]();    while (result.length < count) {        const {value, done} = await iterator.next();        if (done) break;        result.push(value);    }    return result;}

示例:

function timeout(v) {    return new Promise((res) => {        setTimeout(() => {            res(v)        }, 1000)    })}async function* gen() {    yield timeout("hello");    yield timeout("async");    yield timeout("iteration");};let asyncIterable = gen()takeAsync(asyncIterable, 5).then((res) => {    console.log(res)    // ["hello", "async", "iteration"]})

现在有没有一种感觉,Generator 函数、async 函数和异步 Generator 函数好像懂了,但同时也乱了。

JavaScript的几种函数

普通函数、async 函数、Generator 函数和异步 Generator 函数。

Generator 函数
1.执行它会返回遍历器对象,遍历器对象(包括原生)的Symbol.iterator 属性的值就是 Generator 函数

2.可以通过遍历器对象的next方法获取结果,也可以通过多种遍历方式调用

3.应用之一是:可以简化异步操作。通过Generator、Promise对象/Thunk函数、执行器,实现Generator函数自动执行

async 函数
1.它是 Generator 函数的语法糖。其实就是自带执行器的Generator函数。

2.直接调用,它会返回一个Promise对象。

异步操作的同步写法

异步 Generator 函数
1.执行它会返回异步遍历器对象

2.可以通过遍历器对象的next方法获取结果,只是方式不同,也可以通过for await...of调用

3.异步 Generator 函数 比 Generator 函数,更方便处理异步操作,在调用next方法时,前者把 Promise对象作为 value属性返回,后者直接返回Promise。

yield*

function timeout(v) {    return new Promise((res) => {        setTimeout(() => {            res(v)        }, 1000)    })}async function* gen() {    yield timeout("hello");    yield timeout("async");    yield timeout("iteration");};async function* asyncGen() {    yield timeout("1");    yield timeout("2");    yield* gen()}for await (var x of asyncGen()) {    console.log(x)}// 1// 2// hello// async// iteration

Class 基本语法

类,构造函数,原型

用ES5的方法生成一个实例对象

function Person (name, age) {    this.name = name    this.age = age}Person.prototype.toString = function () {    return '姓名是:' + this.name + '\n年龄是:' + this.age}let p = new Person('luyuan', 18)p.toString()    // "姓名是:luyuan, 年龄是:18"Person.prototype    // {toString: ƒ (), constructor: ƒ Person(name, age)}Person.prototype.constructor === Person  // true

用ES6的方法生成一个实例对象

class Person {    constructor (name, age) {        this.name = name        this.age = age    }        toString () {        return '姓名是:' + this.name + '\n年龄是:' + this.age    }}let p = new Person('luyuan', 18)p.toString()Person.prototype    // {constructor: class Person, toString: ƒ toString()}Person.prototype.constructor === Person  // true

1.ES5的构造函数对应Class中的constructor方法,原型对应Class中的方法。

注意:

Class中定义方法,不需要加 function
不需要加逗号

ES6 的类,就是构造函数新的写法。

2.构造函数的原型的构造器是构造函数自身,类的原型的构造器是类自身

3.使用方法都是通过new调用

4.实例对象p调用的方法都在原型上

区别:
1.构造函数不用new 也可以执行,Class不行。

constructor

通过new命令调用Class时,默认调用Class的constructor方法。

类与实例

1.定义在this上的属性是实例本身的属性,其他都在原型上

p.hasOwnProperty('name') // truep.hasOwnProperty('age') // truep.hasOwnProperty('toString') // false

name, age是this的属性,hasOwnProperty方法返回true

2.同一个类实例化出的多个实例对象,共用一个原型对象

let p1 = new Person('luyuan', 18)let p2 = new Person('shuaijie', 23)p1.__proto__ === p2.__proto__       // true

proto 不属于语言的标准,是浏览器厂商添加的扩展属性,可以使用Object.getPrototypeOf()来代替

Object.getPrototypeOf(p1) === Object.getPrototypeOf(p2) // true

getter 和 setter

class Person {    constructor (name, age) {        this.name = name        this.age = age    }        get age () {        return '18'    }    set age (v) {        console.log('setter: '+ v);    }}let p = new MyClass();p.age = 22;// setter: 22p.age// 18

使用get和set,对某个属性设置存值函数和取值函数,可以拦截该属性默认的存取行为。

Class 表达式

const MyClass = class Me {...};

const MyClass = class { /* ... */ };
有什么区别?
前者,在外部只能通过MyClass引用。在内部可以通过Me和MyClass使用
后者在内外只能通过MyClass引用

立即执行的 Class

let person = new class {    constructor(name) {        this.name = name;    }    sayName() {        console.log(this.name);    }}('张三')person.sayName()// 张三

注意点

1.类和模块的内部,默认就是严格模式,
2.类不存在变量提升

new Foo(); // ReferenceErrorclass Foo {}

3.Class继承了函数的许多特性,包括name属性
4.Generator 方法

class Func {    constructor (...args) {        this.args = args;    }    * [Symbol.iterator] () {        for (let arg of this.args) {            yield arg;        }    }}let iterator = new Func('hello', 'world')for (let x of iterator) {  console.log(x);}// hello// world

5.this的指向
this默认指向了类的实例,但是当方法单独调用时,this就会变成undefined。

class Person {    printName (name = '') {        this.print(`hello, ${name}`)    }        print(text) {        console.log(text);    }}let p = new Person()p.printName('luyuan')   // hello, luyuanlet { printName } = pprintName('luyuan') // TypeError: Cannot read property 'print' of undefined

解决办法:
1) 在构造方法中绑定this

class Person {    constructor () {        this.printName = this.printName.bind(this)    }    printName (name = '') {    }        //...}let p = new Person()p.printName() === p // true

2) 使用箭头函数

class Person {    constructor () {        this.PrintName = () => this    }}let p = new Person()p.printName() === p // true

static 方法

方法前面有static关键字的就是 静态方法,它不能被继承,也就是只能通过类来调用。

静态方法中的this指的是类本身,不会是其他对象

静态方法可以和非静态方法同名

子类可以继承父类的静态方法

可以在子类中覆盖父类的静态方法,
也可以通过super调用父类的静态方法

实例属性

属性可以直接定义到constructor的this上面
属性可以直接定义到类的最顶层

class Func {    a = 1    constructor () {        this.b = 1    }}

static 属性

行为同静态方法一样

私有方法和私有属性

现有的解决方案

在命名上区分,好比构造函数与普通函数差别就是首字母是否大写。

私有方法和私有属性则是以 _ 为首

私有属性的提案

在方法或者属性前面加上 # 关键字。

new.target 属性

1) 表示new命令作用于的那个构造函数

通过这个属性可以让构造函数必须通过new命令调用

function Person(name) {    if (new.target !== undefined) {        this.name = name;    } else {        throw new Error('必须使用 new 命令生成实例');    }}Person()    // Uncaught Error: 必须使用 new 命令生成实例new Person()

2) 子类继承父类时,new.target会返回子类

通过这个属性可以实现不允许直接实例的类

Class 的继承

ES5 构造函数、原型对象和实例之间的关系

先来了解一下 ES5 构造函数、原型对象和实例之间的关系(解释)

function Child () {    }let child = new Child()child.__proto__ === Child.prototype   // truechild.constructor === Child.prototype.constructor //true

通过原型链来实现继承

function Child () {}let child = new Child()// 实例原型链child.__proto__ === Child.prototype   // trueChild.prototype.__proto__ === Object.prototype //trueObject.prototype.__proto__ === null; // true// 构造器原型链Child.__proto__ === Function.prototype  // trueFunction.prototype.__proto__ === Object.prototype; // trueObject.prototype.__proto__ === null; // true

构造函数的方法,只能通过构造函数来调用,而构造函数原型上的方法,则可以通过实例调用。

// Array的实例可以调用forEach方法["a", "s", "d", "f"].forEach(item => {    console.log(item)})// a, s, d, f// Array可以调用Array.from()Array.from('asdf')  // ["a", "s", "d", "f"]

ES6 类、原型、实例之间的关系

再看一下 ES6 类、原型、实例之间的关系

class Parent {    }let parent = new Parent()parent.__proto__ === Parent.prototype   // trueparent.constructor === Parent.prototype.constructor // true

ES6 类的继承

类同时有prototype属性和__proto__属性,因此同时存在两条继承链,一条是构造函数的继承,一条是原型方法的继承
1) 子类的__proto__属性,表示构造函数的继承,总是指向父类。

2) 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

具体看一下这俩个原型链

class Parent {}class Child extends Parent {}let child = new Child()// 实例原型链child.__proto__ === Child.prototype // trueChild.prototype.__proto__ === Parent.prototype; // trueParent.prototype.__proto__ === Object.prototype //trueObject.prototype.__proto__ === null; // true// 构造器原型链Child.__proto__ === Parent  // trueParent.__proto__ === Function.prototype // trueFunction.prototype.__proto__ === Object.prototype   // trueObject.prototype.__proto__ === null; // true

主要解释一下⑥和⑦
⑥的含义,子类的原型是父类,体现在,通过子类可以获取父类的静态方法或者静态属性

class Parent {    static age = 45    static say() {        console.log(Parent.age)    }}class Child extends Parent {}Child.age   // 45Child.say() // 45

⑦的含义,子类有子类的原型对象,父类有父类的原型对象,父类的原型对象是子类原型对象的原型。只有这个条件满足时,子类的实例才能获取到父类原型的方法和属性(不理解可以看上面 ES6 类的继承中实例原型链)

class Parent {    constructor () {        this.name = 'Parent'    }    say () {        console.log(this.name)    }}class Child extends Parent {    constructor () {        super()    }}let child = new Child()child.name  // Parentchild.say() // Parent

面试官问:JS的继承

实例 与 子类和父类的关系

ES6子类的实例对象同时是父类的实例

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置
child instanceof Child  // truechild instanceof Parent // true

extends

extends关键字可以继承目标类所有属性和方法

super

1) super作为函数时

为什么继承必须要调用父类的构造函数?
文档上的解释:自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

我的理解:就是先实例化父类,然后返回this对象,然后在子类的构造函数中修饰this,这样也就实现了原型链的继承

例子

class Parent {    constructor () {        console.log(new.target.name, 'new.target.name');    }}class Child extends Parent {    constructor () {        super()    }}let child = new Child()// Child new.target.name

new.target 回返回 new调用的那个构造函数,在这里是Child

2) super作为对象时

可以调用父类原型上的属性和方法,定义在构造函数中的属性和方法无法调用

使用super调用父类的方法时,this指向子类实例

在静态方法之中,super指向父类,调用父类方法时,内部的this指向了父类

可以在普通对象中使用super调用原型的方法

var obj = {    toStirng() {        return super.toStirng()    }}obj.toString()  // "[object Object]"

原生构造函数的继承

定义一个带版本功能的数组

class VersionedArray extends Array {    constructor() {        super();        this.history = [[]];    }    commit() {        this.history.push([...this]);    }}var x = new VersionedArray();x.push(2)x.push(1)x.commit()x.history// [Array(0), Array(2), Array(3)]// 0: []// 1: (2) [2, 1]// 2: (3) [2, 1, 3]

混合 Mixin

将多个类的接口混入另一个类。

function mix(...mixins) {    class Mix {        constructor() {            for (let mixin of mixins) {                copyProperties(this, new mixin()); // 拷贝实例属性            }        }    }    for (let mixin of mixins) {        copyProperties(Mix, mixin); // 拷贝静态属性        copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性    }    return Mix;}function copyProperties(target, source) {    for (let key of Reflect.ownKeys(source)) {        if ( key !== 'constructor'            && key !== 'prototype'            && key !== 'name'        ) {            let desc = Object.getOwnPropertyDescriptor(source, key);            Object.defineProperty(target, key, desc);        }    }}

混入A和B两个类

class A {    constructor () {    }    sayName () {        console.log(this.name)    }}class B {    constructor () {    }    static height = 180    read () {        console.log('reading')    }    static listen () {        console.log('listen')    }}class Test extends mix(A, B) {}Test.height // 180Test.listen()   // listenlet t = new Test()t.name = 'shuai't.sayName()     // shuait.read()    // reading