简单介绍
分享内容可分为两块:优雅的异步编程
和 JavaScript的类
Generator
是 ES6
的一种异步编程的方案, ES2017
引入了 async
函数,它是 Generator
函数的语法糖,它让异步操作更加方便。
JavaScript
在 ES6
之前一直没有类的概念,生成实例的方法就是通过构造函数。但是 类(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}
返回一个有 value
和 done
两个属性的对象。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方法,会返回一个对象,对象有 value
和 done
属性,这里的 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命令和错误处理
- 如果await后面是Promise对象,则返回对象的结果,如果是原始类型的值,则返回对应的值。
示例 vue-app-train/Async.vue demo3()
- 如果await后面是一个定义 then 方法的对象,await 将其等同于Promise对象。
示例 vue-app-train/Async.vue demo4()
- await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
示例 vue-app-train/Async.vue demo2()
- 任意一个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()
注意点
- await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中。已经介绍过了,有两种写法
- 多个await命令后面的异步操作,如果没有依赖关系,可以同时触发。 示例
train/node/server/libs/asyncReaedFileOpt.js readFileAsyncFunc()
- await 在普通函数中会报错
- 在forEach方法中和for循环中使用async,await。示例
train/node/server/libs/asyncReadFileOpt.js dbFuc()
- 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
发表回复