优雅的异步编程和JavaScript的类

62次阅读

共计 25360 个字符,预计需要花费 64 分钟才能阅读完成。

简单介绍

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

正文完
 0