【前端】我的笔记──迭代器和生成器

前言

什么是迭代器和生成器,其实这里有很多概念是大家经常不理解和忽略的地方。

迭代器其实就是一种类似于Promise+规范,迭代器是一个复合迭代器规范的函数,是不是有点拗口。迭代器函数返回一个含有next方法的对象,next方法返回一个包含 value done 属性的对象。

生成器就是 function * ( ){}格式的,加上yield进行暂停。

1.迭代

1.1 什么是迭代

你给我翻译翻译什么叫做迭代,迭代就是按照顺序遍历执行的具有终止条件的一段程序。记住三个特点:有序、循环遍历以及终止条件。

  • 有序:迭代会在一个有序集合上进行。“有序”指的是集合中所有项均可按照定义的顺序进行遍历,其中开始项和结束项是明确定义的。(如数组就是有序集合)
  • 循环遍历:每次循环都是在下一次迭代开始之前完成。
  • 终止条件:循环遍历的次数。(没有终止条件的循环是死循环,那不完犊子了吗)
const colors = ["red","green","blue"];
colors.forEach(item=>console.log(item));
//"red"
//"green"
//"blue"
复制代码

1.2 数组迭代的缺陷

  • 迭代前需要事先知道如何使用数据结构。
  • 不适用于除数组之外的隐式顺序的数据结构。

1.3 ES5中的数组迭代

  • forEach()
  • map()
  • some()
  • every()
  • filter()

2.迭代器

2.1 迭代器和可迭代对象

迭代器指的是被设计来专门迭代的对象,带有特定的接口。即:按需创建的一次性对象。

可迭代对象指的是实现了正式的Iterbale接口,并且可以通过迭代器Iterator消费的结构。说人话,就是可以遵循某些规则从中取出数据的对象。

这些规则就是:

  • 主对象/类应该存储一些数据。
  • 主对象/类必须具有全局的Symbol,且这个属性必须使用特殊的Symbol.iterator作为键。
  • Symbol.iterator方法必须一个新的迭代器对象──“默认迭代器”。
  • 迭代器对象必须有一个next()方法
  • 每次成功调用next(),都必须返回迭代结果对象,其中包含迭代器返回的下一个值
  • 若不调用next(),则无法知道迭代器的当前位置

每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。

2.2 可迭代协议

可迭代协议具有两种能力:

  • 支持迭代的自我意识。用于解决迭代前需要事先知道数据结构类型的缺陷。
  • 创建实现可迭代接口的对象的能力。用于解决迭代不适用于除数组之外的隐式顺序的数据结构。

其实,就是通过在对象或对象原型链上暴露“默认迭代器”属性来获取迭代行为,这个属性使用特殊的Symbol.iterator作为键,调用迭代器工厂函数返回一个新的迭代器。

内置可迭代对象接口的类型

  • String
  • Array
  • Map
  • Set
  • argument
  • NodeList等DOM集合对象

接收可迭代对象的语法

  • for-of循环
  • 数组解构
  • 扩展运算符
  • yield* 操作符,在生成器中使用
  • Array.from()
  • 创建集合new Map()
  • 创建映射new Set()
  • Promise.all()接收由Promise组成的可迭代对象
  • Promise.race()接收由Promise组成的可迭代对象

这些语法结构会在后台调用提供的可迭代对象的工厂函数,从而创建一个迭代器。

自定义可迭代对象

如果对象原型链上的父类也实现了Iterable接口,那么这个对象也就实现了可迭代接口。

class SonArray extends Array{}
const sonArray = new SonArray("zhangsan","lisi","waner");
for(let item in sonArray){
  console.log(item);
}
// "zhangsan"
// "lisi"
// "wenger"
复制代码

2.3 迭代器协议

我们知道,迭代器是一种一次性使用的对象,用于迭代与其相关的可迭代对象。

一句话说明就是:只有实现next()方法的对象才能成为迭代器。

next()调用成功后返回两个值,分别是:done和value。done表示的就是本次next调用成功,可以进行下一次next()的调用;value表示的是包含下一次调用next()的值。(done为false时,value为对应的值;done为true时,value为undefined)

举例子:

//可迭代对象
const colors = ["red","green","blue"];
//迭代器工厂函数,获取迭代器
let iter = colors[Symbol.iterator]();
//执行迭代
console.log(iter.next());//{done:false,value:"red"};
console.log(iter.next());//{done:false,value:"green"};
console.log(iter.next());//{done:false,value:"blue"};
复制代码

我们看到,这里通过创建迭代器并且调用了next()方法进行顺序迭代数组,直到遍历完所有的值(即:done为true时停止迭代)。

迭代器的特点

  • 每个迭代器都表示对可迭代对象的一次性有序遍历。
  • 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。

迭代器的概念比较模糊,有时候指的是迭代,有的时候指的是接口,还可以指迭代器类型。那么迭代器的真面目是:

class Colors{
  // 迭代器
  [Symbol.iterator] () {
    return {
    	// next() 函数
      next () {
      	// 返回 IteratorReault
        return { done: false, value: 'red' };
      }
    }
  }
}
//创建一个实例对象
const colors = new Colors();
//打印实现了迭代器接口的对象
console.log(colors[Symbol.iterator]()); // { next: colors () {} }
//
console.log(color[Symbol.iterator]().next()); // {done: false, value: "red"}

复制代码

2.4 自定义迭代器

任何实现Iterator接口的对象都可以作为迭代器使用,为了能够将可迭代对象迭代多次,需要迭代器对象上创建对个迭代器,每一个迭代器对应一个新计数器,再将计数器变量放到闭包中,通过闭包返回迭代器。

例如:

class Counter {
  constructor (limit) {
    this.limit = limit;
  }

  [Symbol.iterator] () {
    let count = 1,
      limit = this.limit;
    retrun  {
      next () {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      }
    }
  }
}

let counter = new Counter(3);
for(let i of counter){
  console.log(i);
}
// 1
// 2
// 3
复制代码

这样,每个以这种方式创建的迭代器均实现了可迭代接口。

2.5 提前终止迭代器

迭代器的可选return()可以用于指定让迭代器提前终止执行迭代。

  • for-of 循环通过 break\continue\return\throw 等提前退出
  • 解构赋值并未消费所有值

例如:

class Counter {
  constructor (limit) {
    this.limit = limit;
  }

  [Symbol.iterator] () {
    let count = 1,
      limit = this.limit;
    return {
      next () {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
      // 加入 return
      return () {
      	console.log('迭代器关闭前执行操作');
        return { done: true };
      }
    }
  }
}

let counter = new Counter (5);

for (let i of counter) {
  if (i > 2) break;
  console.log(i);
}
// 1
// 2
// 迭代器关闭前执行操作

复制代码

注意:

  • 没有设定 return() 关闭迭代器或迭代器不能关闭的情况下,迭代退出后还可从上次离开的地方继续迭代
  • 设定了 return() 的迭代器不会强制进入关闭状态,但 return() 会被调用

2.生成器

前面已经将迭代器的基本概念和特性已经整理清楚,接下来我们开始讲解生成器。

2.1 什么是生成器

生成器(Generator)函数是es6提供的异步编程解决方案,语法和传统函数不同。生成器函数是一个状态机,封装了多个内部状态,通过yield暂停执行,通过next()进行下一次迭代。

简而言之,生成器就是用来控制迭代器的函数,可以随时暂停,随时恢复。

生成器也可以看作是一个值的生产者,通过迭代器接口的next()可以调用一次取得一次值。

如何定义一个生成器,很简单在函数定义前面加上* 即可:

//函数声明式生成器
function *fun(){}
//函数表达式生成器
let fun = function*(){}
//字面量式生成器
let fun = {
  * foo(){}
}
//等等
复制代码

2.2 生成器的特点

  • 标识生成器函数的星号* 不受两边空格影响
  • 箭头函数不能用于定义生成器函数(着重注意,所以箭头函数没有arguments属性)
  • 调用生成器函数会产生一个生成器对象
  • 生成器对象初始状态处于暂停执行(suspended)状态
  • 生成器对象内置Iterator接口,具有next()方法
  • 函数体内部使用yield表达式,定义不同的内部状态
  • 生成器函数时分段执行的,调用next()方法函数内部逻辑开始执行
  • 函数体为空的生成器函数中间不会停留,调用一次next()就到达done:true状态
  • 生成器对象只会在初次调用next()方法后才会执行
  • 生成器对象实现了可迭代接口,默认迭代器是自引用的

例如:

let generatorFn = function* () {
  console.log('执行');
  return 'generator'
};

const g  = generatorFn();
console.log(g); // generatiorFn {<suspended>}
console.log(g.next);// ƒ next() { [native code] }
console.log(g.next());
// 执行
// {value: "generator", done: true}

console.log(generatorFn);
/* ƒ* () {
	console.log('执行');
    return 'generator'
}*/
console.log(generatorFn()[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(generatorFn()); // generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>}

复制代码

我们看下Generator 函数是如何实现分段执行的。

function *Gen(){
  yield "wenbo";
  yield "bowen";
  yield "zhaoshun";
  return "ending";
}

const gen = Gen();
gen.next();//{done:false,value:"wenbo"}
gen.next();//{done:false,value:"bowen"}
gen.next();//{done:false,value:"zhaoshun"}
gen.next();//{done:true,value:undefined}
复制代码

我们看到前三次调用next()都会遇到yield,此时函数会暂停执行,分别返回一个valuedone。判断done的值不是true,说明迭代还没停止,此时继续遍历。

当执行第四次next()时遇到return(如果没有return,表示直到函数执行结束)。next()方法返回的对象的value属性,紧跟在return语句后面表达式的值,此时done的值为true,表示遍历结束。

yield中断执行

yield关键字可以让生成器停止和开始执行。生成器函数初次执行时,当遇到yield会暂停执行,函数作用域的状态会被保留。当再进行调用next()方法时,生成器函数只有通过在生成器对象调用next()方法来恢复执行。

生成器对象作为可迭代对象
在生成器对象可以不同显式调用next(),可以把迭代器对象当成可迭代对象。

function *Gen(){
  yield "wenbo";
  yield "bowen";
  yield "zhaoshun";
}
for(const item of Gen()){
  console.log(item);
}
// "wenbo"
// "bowen"
// "zhaoshun"
复制代码

使用yield实现输入输出
yield既可以作为函数的中间返回语句使用,也可以作为函数的中间参数使用。第一次调用next()的传入的值不会被使用,是为了执行生成器函数。

function *Gen(init){
  console.log(init);
  console.log(yield);
  console.log(yield);
}

const gen = Gen("fun");
gen.next("wenbo");//wenbo
gen.next("bowen");//bowen
gen.next("zhaoshun");//zhaoshun
复制代码

产生可迭代对象 可以使用星号* 增强yield的行为,能够让那迭代一个可迭代对象,从而每次产出一个值。

因为yield* 实际上只是将一个可迭代对象序列化为一串单独产出的值,所以放在循环中无异。

function *Gen(){
  for(const x of [1,2,3]){
    yield x;
  }
}
//等价于
function *Gen(){
  yield* [1,2,3];
}
复制代码

在生成器对象中实现了Iterator接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器各位适合作为默认迭代器。由于 Generator 函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

let obj = { username: 'wenbo', age: 19 }
obj[Symbol.iterator] = function* myTest() {
  yield 1;
  yield 2;
  yield 3;
};
for (let i of obj) {
  console.log(i) // 1 2 3
}

复制代码

提前终止生成器

一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。return()throw()方法均可以实现强制生成器进入关闭状态。

  • return()方法会强制生成器进入关闭状态。提供给return()方法的值,就是终止迭代器对象的值
  • throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未处理,生成器就会被关闭。
function* Gen(){
  for(const x of ["red","green","blue"]){
    yield x;
  }
}
const gen = Gen();
console.log(gen.next());//{done:false,value:"red"}
console.log(gen.return("yellow"));//{done:false,value:"yellow"}
console.log(gen.next());//{done:true,value:undefined}
console.log(gen.next());//{done:true,value:undefined}


function *Fun(){
  for(const x of ["red","green","blue"]){
    yield x;
  }
}
const fun = Fun();
console.log(fun);//Fun {<suspended>}
try{
  fun.throw("yellow");
}catch(e){
  console.log(e);//yellow
}
 console.log(fun);//Fun {<closed>}
复制代码

应用场景

协程场景

function  schoolTask(goSchool,endSchool){
   let goSchoolIterator=goSchool();
   let endSchoolIterator=endSchool();
   console.log(goSchoolIterator.next().value);
   console.log(endSchoolIterator.next().value);
   console.log(endSchoolIterator.next().value);
   console.log(endSchoolIterator.next().value);
   console.log(goSchoolIterator.next().value);
   console.log(endSchoolkIterator.next().value);
}
function  *goSchool(){
   yield "我上了小学";
   yield "我上了中学";
   yield "我上了大学";
}

function  *endSchool(){
   yield "我小学毕业了";
   yield "我中学毕业了";
   yield "我大学毕业了";
}

schoolTask(goSchool,endSchool);
/*
我上了小学
我小学毕业了
我上了中学
我中学毕业了
我上了大学
我大学毕业了
*/

复制代码

异步操作同步化

ajax异步操作同步化的核心就是:Generator函数里的yield关键字,它可以中断执行!

其实就是:

当迭代器it第一次调用了next()方法,启动生成器,ajax就会发起请求,当ajax完成时,通过回调函数,它就会再次启动next()方法,将接收到的数据,赋值给上一次yield表达式,于是变量result就收到了ajax发送过来的请求,最后把请求到的数据进行格式化并输出。

function* main() {
   var result = yield request("http://some.url");
   var resp = JSON.parse(result);
   console.log(resp.value);
}

function request(url) {
   makeAjaxCall(url, function(response){
       it.next(response);
   });
}

var it = main();
it.next();

复制代码

小结

  • 迭代器:一个可以由任意对象实现的接口,支持连续获取对象产出的一个只。任何实现Iterable接口的对象都有一个Symbol.iterator属性,这个属性引用默认迭代器。默认迭代器就系那个一个迭代器工厂,调用之后就会产生一个实现Iterator接口的对象。
  • 生成器:一种调用之后会返回一个生成器对象的特殊函数。生成器对象实现了Iterable接口,因此可用于任何消费可迭代对象的地方。生成器支持yield关键字,能够暂停执行生成器函数;使用yield可以通过next()方法接受输入和产生输出。

参考文章

《ES6迭代器和生成器》

《[JS红宝书笔记]迭代器和生成器》

《JavaScript高级程序设计(第四版)》

阮一峰《ES6标准入门》

写在最后

感谢大家的阅读,我将继续和大家分享更多优秀的文章,期待大家的关注,咨询交流请联系微信:codepanda2020。

最后感谢大家对于本公众号的大力支持,不忘初心,继续热爱,也希望大家能够点亮我的公众号的【大拇指】、【小方块】、【小星星】,嘿嘿,感谢。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享