【ES6 笔记】迭代器和生成器
迭代器
概念:迭代器是一种特殊对象,包含一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value,表示下一个将要返回的值;一个是done,布尔值,当没有更多可返回的数据时返回true。
function createIterator(items){
var i = 0 ;
return {
next : function (){
var done = (i>=items.length);
var value = !done?items[i++]:undefined;
return {
done,
value
}
}
}
}
var iterator = createIterator([1,2]) ;
console.log(iterator.next()); // {done: false, value: 1}
console.log(iterator.next()); // {done: false, value: 2}
//之后所有的调用都会返回相同的内容
console.log(iterator.next()); // {done: true, value: undefined}
上面只是原理,ES6中引入了生成器来简化创建迭代器的过程
生成器
概念:生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格:
// 生成器
function *createIterator(){
yield 1;
yield 2;
}
// 生成器的调用方式与普通函数相同,只不过返回的是一个迭代器
var iterator = createIterator() ;
console.log(iterator.next()); //{value: 1, done: false}
console.log(iterator.next()); //{value: 2, done: false}
//之后所有的调用都会返回相同的内容
console.log(iterator.next()); //{value: undefined, done: true}
在这个例子中,星号表示这是一个生成器;yield关键字指定调用迭代器的next()的返回值和返回顺序。
没当执行完一句yield语句后函数会自动停止执行。
function *createIterator(items){
for( let i=0; i<items.length; i++ ){
yield items[i]
}
}
let iterator = createIterator([1,2]) ;
console.log(iterator.next()); // {done: false, value: 1}
console.log(iterator.next()); // {done: false, value: 2}
//之后所有的调用都会返回相同的内容
console.log(iterator.next()); // {done: true, value: undefined}
yield关键字只可在生成器内部使用,嵌套的非生成器函数不算内部
- 生成器函数表达式
let createIterator = function *(items){
for( let i=0; i<items.length; i++ ){
yield items[i]
}
}
let iterator = createIterator([1,2]) ;
console.log(iterator.next()); // {done: false, value: 1}
console.log(iterator.next()); // {done: false, value: 2}
//之后所有的调用都会返回相同的内容
console.log(iterator.next()); // {done: true, value: undefined}
不能用箭头函数来创建生成器
-
生成器对象方法
由于生成器本身就是函数,因而可以将它们添加到对象中。
let obj = {
createIterator : function *(items){
for( let i=0; i<items.length; i++ ){
yield items[i]
}
}
}
let iterator = obj.createIterator([1,2]) ;
简写法:
let obj = {
*createIterator(items){
for( let i=0; i<items.length; i++ ){
yield items[i]
}
}
}
let iterator = obj.createIterator([1,2]) ;
可迭代对象和for-of循环
可迭代对象具有Symbol.iterator属性,通过制定的函数可以返回一个作用于附属对象的迭代器。
由于生成器默认会为Symbol.iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象。
for-of循环每执行一次都会调用可迭代对象的next()方法,并将迭代器返回的结果对象的value属性存储在一个变量中,循环将持续执行这一过程直到返回对象的done属性的值为true。
let values = [ 1, 2, 3 ]
for(let num of values){
console.log(num)
}
// 分别输出1、2、3
如果将for-of语句用于不可迭代对象、null或undefined将会导致程序抛出错误
-
访问默认迭代器
可以通过Symbol.iterator来访问对象默认的迭代器:
let values = [ 1, 2, 3 ];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // { value : 1, done : false }
console.log(iterator.next()); // { value : 2, done : false }
console.log(iterator.next()); // { value : 3, done : false }
console.log(iterator.next()); // { value : undefined, done : true }
由于具有Symbol.iterator属性的对象都有默认的迭代器,因此可以用它来检测对象是否为可迭代对象:
function isIterable(object){
return typeof object[Symbol.iterator] === 'function';
}
console.log( isIterable([1,2,3]) ) // true
console.log( isIterable("Hello") ) // true
console.log( isIterable(new WeakMap()) ) // false
-
创建可迭代对象
默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator属性添加一个生成器,则可以将其变为可迭代对象:
let collection = {
items : [],
*[Symbol.iterator](){
for(let item of this.items){
yield item
}
}
}
collection.items.push(1);
collection.items.push(2);
for (let x of collection){
console.log(x)
}
// 分别输出1、2
内建迭代器
-
集合对象迭代器
在ES6中有3种类型的集合对象:数组、Map集合和Set集合。
这3种对象都内建了以下三种迭代器:- entries() 返回一个迭代器,其值为多个键值对;
- values() 返回一个迭代器,其值为集合的值;
- keys() 返回一个迭代器,其值为集合中的所有键名。
entries()
每次调用next()方法时,entries()迭代器都会返回一个数组,数组中的两个元素分别表示集合中每个元素的键与值。
如果被遍历的对象是数组,则第一个元素是数字类型的索引;
如果是Set集合,则第一个元素与第二个元素都是值(Set集合中的值被同时作为键与值使用);
如果是Map集合,则第一个元素为键名,第二个元素为键值。
let colors = ['red'];
let numbers = new Set([1234]);
let maps = new Map([['key','value']]);
for (let entry of colors.entries()){
console.log(entry); // [0, "red"]
}
for (let entry of numbers.entries()){
console.log(entry); //[1234, 1234]
}
for (let entry of maps.entries()){
console.log(entry); //["key", "value"]
}
values()
调用values()迭代器时返回集合中所存在的值:
let colors = ['red'];
let numbers = new Set([1234]);
let maps = new Map([['key','value']]);
for (let entry of colors.values()){
console.log(entry); //"red"
}
for (let entry of numbers.values()){
console.log(entry); //1234
}
for (let entry of maps.values()){
console.log(entry); //value
}
keys()
调用keys()迭代器时返回集合中存在的每一个键:
let colors = ['red'];
let numbers = new Set([1234]);
let maps = new Map([['key','value']]);
for (let entry of colors.keys()){
console.log(entry); // 0
}
for (let entry of numbers.keys()){
console.log(entry); // 1234
}
for (let entry of maps.keys()){
console.log(entry); // ‘key’
}
不同集合类型的默认迭代器
每个集合类型都有一个默认迭代器,在for-of循环中,如果没有显式指定则使用默认的迭代器。
数组和Set集合的默认迭代器是values()方法
Map集合的默认迭代器是entries()方法
let colors = ['red'];
let numbers = new Set([1234]);
let maps = new Map([['key','value']]);
for (let entry of colors){
console.log(entry); // red
}
for (let entry of numbers){
console.log(entry); // 1234
}
for (let entry of maps){
console.log(entry); // ["key", "value"]
}
结构与for-of循环
如果要在for-of循环中使用结构语法,则可以利用Map集合默认构造函数的行为来简化编码过程:
let data = new Map();
data.set('name','欧阳不乖')
data.set('age',18)
for(let [key, value] of data){
console.log(`${key} = ${value}`)
}
// name = 欧阳不乖
// age = 18
- 字符串迭代器
let message = 'Hi 欧阳不乖';
for(let w of message){
console.log(w)
}
//H
// i
// (空)
//欧
//阳
//不
//乖
- NodeList迭代器
let divs = document.getElementByTags('div');
for (let div of divs){
console.log(div.id)
}
展开运算符与非数组可迭代对象
在数组字面量中可以多次使用展开运算符,将可迭代对象中的多个元素依次插入新数组中,替换原先展开运算符所在的位置:
let smallNumbers = [ 1, 2, 3 ];
let bigNumbers = [ 100, 101, 102 ];
let allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]
高级迭代器功能
- 给迭代器传递参数
function *createIterator(){
let first = yield 1;
let second = yield first +2;
yield second +3;
}
let iterator = createIterator();
console.log( iterator.next() ); // {value: 1, done: false}
console.log( iterator.next(4) ); // {value: 6, done: false}
console.log( iterator.next(5) ); // {value: 8, done: false}
console.log( iterator.next() ); // {value: undefined, done: true}
这里有一个特例,第一次调用next()方法时无论传入什么样的参数都会被丢弃。
让我们从下面的代码分解步骤,来理解一下代码内部的具体细节:
执行步骤
-
在迭代器中抛出错误
通过throw()方法,当迭代器恢复执行时可令其抛出一个错误。
function *createIterator(){
let first = yield 1;
let second = yield first +2;
yield second +3;
}
let iterator = createIterator();
console.log( iterator.next() ); // {value: 1, done: false}
console.log( iterator.next(4) ); // {value: 6, done: false}
console.log( iterator.throw(new Error('BOOM')) ); //浏览器抛出错误:Uncaught Error: BOOM
console.log( iterator.next() ); //这里什么都不会再发生了
如下图所示,错误抛出则立即阻止了后续代码的执行,红色背景区域的代码永远都不会被执行了:
错误抛出位置示意图
-
生成器返回语句
在生成器中,return表示所有操作已经完成,属性done被设置为true;如果同时提供了相应的值,则属性value会被设置为这个值:
function *createIterator(){
yield 1;
return 'finished'; //之后的代码将不再执行
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: "finished", done: true}
console.log(iterator.next()); // {value: undefined, done: true}
通过return语句指定的返回值,只会在返回对象中出现一次,在后续调用返回的对象中,value属性会被重置为undefined。
展开运算符与for-of循环语句会直接忽略通过return语句指定的任何返回值,只要done一变为true就立即停止读取其他的值。
-
委托生成器
在某些情况下,我们需要将两个迭代器合二为一,这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他迭代器。
当定义这些生成器时,只需将星号放置在关键字yield和生成器的函数名之间即可:
function *createNumberIterator(){
yield 1;
yield 2
}
function * createColorIterator(){
yield 'red';
yield 'blue';
}
function *createCombinedIterator(){
yield *createNumberIterator();
yield *createColorIterator();
yield true;
}
let iterator = createCombinedIterator();
console.log( iterator.next() ); // {value: 1, done: false}
console.log( iterator.next() ); // {value: 2, done: false}
console.log( iterator.next() ); // {value: "red", done: false}
console.log( iterator.next() ); // {value: "blue", done: false}
console.log( iterator.next() ); // {value: undefined, done: true}
yield *也可以直接应用于字符串,例如 yield *‘hello’
异步任务执行
- 简单任务执行器
function run(taskDef){
// 创建一个无使用限制的迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 循环调用next()的函数
function step(){
// 如果任务尚未完成,则继续执行
if(!result.done){
result = task.next();
}
}
// 开始迭代执行
step();
}
run(function *(){
console.log(1);
yield;
console.log(2)
})
// 1
// 2
-
向任务执行器传递数据
给任务执行器传递数据最简单的办法是,把yield返回值传入下一次next()方法的调用:
function run(taskDef){
// 创建一个无使用限制的迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 循环调用next()的函数
function step(){
// 如果任务尚未完成,则继续执行
if(!result.done){
result = task.next(result.value);
step();
}
}
// 开始执行迭代
step();
}
run(function *(){
let value = yield 1;
console.log(value); // 1
value = yield value+3;
console.log(value) // 4
})
- 异步任务执行器