优雅的异步编程和JavaScript的类

简单介绍

分享内容可分为两块:优雅的异步编程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 // 1
y // 2

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2


let 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
// 2
for (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() // 1
f.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       // undefined
f.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:15

f2()    // 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().value
value.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') // true
p.hasOwnProperty('age') // true
p.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: 22

p.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(); // ReferenceError
class 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, luyuan

let { printName } = p
printName('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   // true
child.constructor === Child.prototype.constructor //true

通过原型链来实现继承

function Child () {}
let child = new Child()

// 实例原型链
child.__proto__ === Child.prototype   // true
Child.prototype.__proto__ === Object.prototype //true
Object.prototype.__proto__ === null; // true

// 构造器原型链
Child.__proto__ === Function.prototype  // true
Function.prototype.__proto__ === Object.prototype; // true
Object.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   // true
parent.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 // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype //true
Object.prototype.__proto__ === null; // true

// 构造器原型链
Child.__proto__ === Parent  // true
Parent.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype   // true
Object.prototype.__proto__ === null; // true

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

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

Child.age   // 45
Child.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  // Parent
child.say() // Parent

面试官问:JS的继承

实例 与 子类和父类的关系

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

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

child instanceof Child  // true
child 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 // 180
Test.listen()   // listen

let t = new Test()
t.name = 'shuai'
t.sayName()     // shuai
t.read()    // reading

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理