ES6-iterator/generator/async/await

196 阅读5分钟

iterator 迭代器

let arr = [1,2,3,4];
console.log(arr);

arr在原型上部署了一个迭代器接口Symbol(Symbol.iterator): ƒ values()

构造器(构造器属性):方法

通过arr[Symbol.iterator]可以拿到对应的方法

let iter = arr[Symbol.iterator]
console.log(iter); //ƒ values() { [native code] }
let iter = arr[Symbol.iterator]();
console.log(iter); 

执行方法可以看见原型里有next方法

console.log(iter.next()); //{value: 1, done: false}

执行返回对象,value代表读取的值,false代表还没有执行完

let arr = [1,2,3,4];
let iter = arr[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());

连续执行六次,可以看到以下结果

迭代器:读取数据结构的一种方式,有序的,连续的,基于拉取的一种消耗数据的组织方式(抽取过了就不再访问了)

封装迭代器

function makeIterator(array) {
    var nextIndex = 0;
    return {
        next: function() {
            return nextIndex < array.length ? 
                { value: array[nextIndex++], done: false} :
                {value: undefined, done: true};
        }
    }
}
let iter = makeIterator([5,2,3,4]);

但是这种方式一直next,有点累赘,ES6中借鉴了c++,java,c#,python,有了for...of...循环 -> 迭代

for...of...调用的就是iterator接口

let arr = [1,2,3,4]
for(let i of arr) {
    console.log(i); //1,2,3,4
}

for..in..拿的是下标,遍历对象;for..of..直接拿的值,迭代部署过迭代器接口的数据类型(Array、Map、weakMap、weakSet、类数组[arguments、nodeList]、typeArray),除了对象(对象是无序的)

如果想让对象迭代的话,就要部署迭代器接口

let obj = {
    start: [1,2,3,4],
    end: [5,6,7],
    [Symbol.iterator](){
        let index = 0,
            arr = [...this.start, ...this.end],
            len = arr.length;
        return {
            next(){
                if(index < len) {
                    return {
                        value: arr[index++],
                        done: false
                    }
                }else{
                    return {
                        value: undefioned,
                        done: true
                    }
                }
            }
        }
    }
}
for(let i of obj){
    console.log(i);
}
var arr = [6,7,5,1];
var iter = arr[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
for(let i of arr){        //两种方式都用的话,for of有可能会失效
    console.log(i);
}

...在对象部署迭代器接口的情况下是可以用的,没有部署拓展对象会报错

let arr = [...obj];
console.log(arr);   //[1,2,3,4,5,6,7]

Map结构还是有顺序的,所以可以迭代,通过for of方法拿到键值对

let map = new Map([['a', 1], ['b', 2]]);
for(let [key, value] of map) {
	console.log(key, value);
}

如果对象想要和Map结构一样

let obj = {
	a: 1,
	b: 2,
	[Symbol.iterator]() {
		let nextIdx = 0;
		// 对象转换为Map
		let map = new Map();
		// Object.entries()方法返回的是数组,所以可以用for of迭代
		for(let [key, value] of Object.entries(this)) {
			map.set(key, value);
		}
		// 迭代器对象
		// let mapEntries = map.entries();
		// Map转换为数组,直接拓展map也可以,这样语义化更明显一点
		let mapEntries = [...map.entries()];
		return {
			next() {
				return nextIdx < mapEntries.length ?
					{value: mapEntries[nextIdx ++], done: false} :
					{value: undefined, done: true}
			}
		}
		// console.log(map)
	}
}
for(let i of obj) {
	console.log(i);
}

但是这种方法其实没什么必要,直接用Map不就完了吗 部署迭代器的时候还可以部署其他方法,上面的只是必选项而已

return {
    next() {...},
    return() {
        console.log(1);
	    return {c: 3}
    }
}

for(let i of obj) {
	console.log(i);
	break;
}

return必须要返回一个对象,并且return方法会在迭代出错或者break的时候执行

内部迭代器和外部迭代器 前端是不区分内部迭代器和外部迭代器的(调用方式不一样),但是其他语言有,稍微了解一下

内部迭代器:系统内部定义好的迭代规则,在进行调用的时候,可以一次性拿到所有的遍历元素(for的一系列方法) 外部迭代器:自己部署迭代器接口,一次抽取一次数据

默认调用iterator接口的场合

  1. 拓展运算符...
  2. for of
  3. Array.from()
  4. map/set
  5. Promise.all( [] ) / Promise.race()
  6. yield

generator

生成器:返回迭代器对象

语法:在function和函数名之间加一个*

哪怕什么都不写,返回的也是一个迭代器对象

function *test() {}
let iter = test();
console.log(iter);

yield产出

特征:

一 、返回值是迭代器对象

二 、yield产出不同的内部值,暂停函数运行

迭代器对象一定要和yield(产出)结合使用,只能出现在生成器当中,在普通函数中会报错

yield只有在next调用的时候才会产出,同时会暂停函数运行,因为迭代器是一次次抽取的

function *test() {
	yield 'a';
	console.log(1);
	yield 'b';
	yield 'c';
	return 'd';
}
let iter = test();
console.log(iter.next()); //{value: "a", done: false} 不会打印1,到第一个yield就停止了,到下一个next才会打印
console.log(iter.next()); // 1 {value: "b", done: false}
console.log(iter.next()); //{value: "c", done: false}
console.log(iter.next()); //{value: "d", done: true}
console.log(iter.next()); //{value: undefined, done: true}

yield和return的区别:

都会产出值,return不会被记录到iterator中:yield产出有值,done就是false;yield产出没有值,done就是false,哪怕最后return了一个d出来

yield有记忆的功能,本质是暂停;return是终止函数运行

三 、yield本身并不产出值,undefined

function *test() {
	let val = yield 'a';
	console.log(val);
	yield 'b';
	yield 'c';
	return 'd';
}
let iter = test();
console.log(iter.next()); //{value: "a", done: false} undefined
console.log(iter.next()); //{value: "b", done: false}

想要yield本身产出值,就传递参数

function *test() {
	let value1 = yield 1;
	console.log(value1);
	
	let value2 = yield 2;
	console.log(value2);
	
	let value3 = yield 3;
	console.log(value3);
}

let iter = test();
console.log(iter.next());
console.log(iter.next('one'));
console.log(iter.next('two'));
console.log(iter.next('three'));

第二个next才可以得到第一个yield的赋值,第一个next没必要传值

利用yield进行一系列操作 yield如果直接嵌套在表达式当中,会报错,要加个括号(变成单独的表达式)才行

例1:独立存在

function *test() {
    yield; //直接这样也行,返回的value是undefined
}

例2:

function *test() {
	console.log('hello ' + (yield 123));
}
let iter = test();
console.log(iter.next());
console.log(iter.next());
{value: 123, done: false}
hello undefined
{value: undefined, done: true}

例3:作为参数传递

function *test() {
	foo(yield 'a', yield 'b');
}
function foo(a, b) {
	console.log(a, b);
}
let iter = test();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
{value: "a", done: false} //yield 'a'执行
{value: "b", done: false} //yield 'b'执行
undefined undefined //foo执行
{value: undefined, done: true}

yield产出的值是迭代器对象,就可以用for of遍历

function *test() {
	yield 1;
	yield 2;
	yield 3;
	yield 4;
	return 5;
}
for (let i of test()) {
	console.log(i); // 1 2 3 4 没有5
}

进行iterator接口代码的优化

let obj = {
    start: [1,2,3,4],
    end: [5,6,7],
    [Symbol.iterator]: function *(){
        let index = 0,
            arr = [...this.start, ...this.end],
            len = arr.length;

        while(index < len) {
            yield arr[index++];
        }
    }
}

用generator和yield,替代模拟返回{value: ... , done: ...}对象的代码。因为生成器函数会自动返回iterator对象,只需要用yield产出值即可,不需要再进行条件判断了。

解决回调地狱的问题

function promisify(func) {
    return function(...args) {
        return new Promise( (resolve, reject) => {
            func(...args, (err, data) => {
                if (data) {
                    resolve(data);
                } else {
                    reject(err);
                }
            }
        })
    }
}

let readFile = promisify(fs.readFile);

// readFile('./name.txt', 'utf-8') //之前的做法
//     .then(data => readFile(data, 'utf-8'))
//     .then(data => readFile(data, 'utf-8'))
//     .then(data => console.log(data));
function * read() {
	let value1 = yield readFile('./name.txt', 'utf-8');
	let value2 = yield readFile(value1, 'utf-8');
	let value3 = yield readFile(value2, 'utf-8');
	console.log(value3);
}
let iter = read();

// let a = iter.next();
// 用解构赋值来做,调用next就相当于抽取一次iterator
// let {value: value, done: done} = iter.next();
// 简写
let {value, done} = iter.next();
// value是readFile()函数执行之后的结果,也就是Promisify之后的结果( promise对象,成功就resolve(data))=> 可链式调用

value.then( (val1) => {
	// 这里的val就是第一次readFile('./name.txt', 'utf-8')的结果
	// 传值val1得到第二次读取值成功的结果
	let {value, done} = iter.next(val1);
	value.then( (val2) => {
		let {value, done} = iter.next(val2);
		value.then( (val3) => {
			console.log(val3);
		})
	})
})

但是这么写还不如之前的代码,所以需要进行代码优化

function Co(iter) {
	return new Promise( (resolve, reject) => {
		// 之前频繁的调用了next
		let next = (data) => {
			let {value, done} = iter.next(data);
			// 递归的出口,done为true,结束
			if (done) {
				resolve(value);
			} else {
				value.then( (val) => {
					next(val);
				})
			}
		}
		// 首先执行一遍,不要参数,启动递归
		next();
	})
}
let promise = Co(read());

promise.then( (val) => {
	// 一步到位
	console.log(val);
})

Co是一个模块,可以下载安装

return

function *get() {
    yield 1;
    //return 10
    yield 2;
    yield 3;
}
let g = get();
console.log(g.next());
console.log(g.return(10));
console.log(g.next());
console.log(g.next());
console.log(g.next());
{value: 1, done: false}
{value: 10, done: true}
{value: undefined, done: true}
{value: undefined, done: true}
{value: undefined, done: true}

return会结束遍历器的迭代,和在函数里return一样,相当于显示调用return

return也相当于yield一次,产出值,但是产出的done是true,不在迭代器当中

throw

unction *get() {
	try {
		yield 1;
	} catch(e) {
		console.log('生成器内部异常:'+ e);
	}
	yield 2;
}
let g = get();
console.log(g.next());
console.log(g.throw('a'));
console.log(g.next());

结果:Uncaught a

throw在第一次next之前是不能捕获到异常的,因为第一次next是yield产出值,next(产出值)之后才会在try的代码区间中。两行掉个顺序就可以正常捕获

结果:

{value: 1, done: false}
生成器内部异常:a
{value: 2, done: false}{value: undefined, done: true}

throw不仅可以抛出异常,也相当于一次next迭代,所以打印异常之后还会产出一次值(拿到yield 2的值)

一般的try catch是不可以捕获到异步代码的异常的。但是在生成器中可以

let fs = require('fs');
let util = require('util');
let co = require('co');

let readFile = util.promisify(fs,readFile);

function *read() {
	try {
		let value1 = yield readFile('./name.tx', 'utf-8');
		let value2 = yield readFile(value1, 'utf-8');
		let value1 = yield readFile(value2, 'utf-8');
	} catch(e) {
		console.log('生成器内部异常:'+ e);
	}
	return value3;
}
let promise = co(read());
promise.then( (val) => {
	console.log(val);
})

async

await就相当于co,co被封装到await内部,作用就是等待异步函数的执行,然后将结果赋值(本质上是generator[语法糖],底层的操作和之前一样)

let fs = require('fs');
let util = require('util');

let readFile = util.promisify(fs,readFile);

async function read() {
	let value1 = await readFile('./name.tx', 'utf-8'); //传错值,这个函数的Promise对象就有了reject状态,然后传递状态
	let value2 = await readFile(value1, 'utf-8');
	let value3 = await readFile(value2, 'utf-8');
	
    console.log(value1 ); //打印出来的结果是字符串
	return value1; //相当于return Promise.resolve(value1)
}
let promise = read();
promise.then( (val) => {
	console.log(val); //打印出来的结果是promise对象,产出的时候会直接转成promise对象
}, (e) => {    //根据返回的promise的状态(reject)进行报错
        console.log(e);
})

根据返回的promise的状态进行相应的回调

特点:

  1. 有内置的执行器:co
  2. 语义好太多了
  3. 更广的适用性:原本的co是一个模块,也就是函数,必须传入一个promise对象,yield后面一定要是promise对象;await后面不仅可以是promise对象
  4. 返回值一定是promise对象:不管await之后的是什么值,最后return都会通过Promise.resolve包装成promise对象 捕获错误:通过Promise的reject的状态调用失败的回调或者try catch都可以,精准捕获还是要用catch,reject只要其中有一个Promise错误就会捕获 通过reject捕获的错误和通过catch捕获的错误是不一样的:

reject是通过传递状态,触发相应的失败的回调函数,出错的地方之后的代码不会继续执行 catch是捕获到错误,立即处理了 如果异步操作没有依赖关系(不是回调地狱),即使有一个出错,还想拿到剩下的异步操作的结果

let f = Promise.all([
	readFile('./name.txt', 'utf-8'),
	readFile('./name.txt', 'utf-8'),
	readFile('./name.txt', 'utf-8')
])
async function readAll() {
	let value1, value2, value3;
	let res = new Set();

	try {
		value1 = await readFile('./name.txt', 'utf-8');
	} catch {
		console.log(e);
	}
	
	try {
		value2 = await readFile('./number.txt', 'utf-8');
	} catch {
		console.log(e);
	}

	try {
		value3 = await readFile('./data.txt', 'utf-8');
	} catch {
		console.log(e);
	}

	res.add(value1);
	res.add(value2);
	res.add(value3);

	return res;
}

readAll().then( (val) => {
	console.log(val);
})

但是一般不怎么做,还是用Promise.all