简单介绍
分享内容可分为两块:优雅的异步编程
和 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 // 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命令和错误处理
- 如果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: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