JavaScript之迭代器和生成器

吐槽君 分类:javascript

迭代器(遍历器)

什么是迭代器?

首先迭代器是一个对象(它很特殊),这个对象可以遍历类数组的数据结构

其次这个对象里有两个方法

  • next() //遍历数据结构里的成员
  • return () //终止遍历

为什么要迭代器呢?

可以提高开发效率!开发者不需要知道数据结构的结构,就能按一定的顺序拿到数据结构里的数据。是不是很高级?

那要怎么要生成一个迭代器呢?

要知道怎么生成一个迭代器,先要知道什么是可迭代协议

实现Iteratable接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator接口的对象的能力。在ES中,这意味着必须暴露一个属性作为"默认迭代器",而这个属性必须使用特殊的Symbol.iterator作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

大白话就说是,创建一个可迭代的对象时,这个可迭代对象必须要自己定义一个迭代器工厂函数,这样就可以利用自己的迭代器来遍历自己的数据了!

很多内置类型都是可迭代对象,意思就是说它们都有自己内置的迭代器!,比如

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments对象
  • NodeList等DOM集合类型

现在就来玩一玩迭代器!

let array=["abc","hello","hy","world"];
let arrayIterator=array[Symbol.iterator]();
/*
调用数组的迭代器工厂函数,生产出一个数组迭代器
*/
console.log(arrayIterator.next());//{ value: 'abc', done: false }
console.log(arrayIterator.next());//{ value: 'hello', done: false }
console.log(arrayIterator.next());//{ value: 'hy', done: false }
console.log(arrayIterator.next());//{ value: 'world', done: false }
console.log(arrayIterator.next());//{ value: undefined, done: true }
 

是不是很神奇呢!,上面代码还可以继续优化一下!

let array=["abc","hello","hy","world"];
let arrayIterator=array[Symbol.iterator]();

for(value of  array){
    console.log(value);
}
/*
abc
hello
hy
world
*/
console.log("------------------------------")
for(value   of  arrayIterator){
    console.log(value);
}
/*
内置类型的迭代器本身也是一个可迭代对象,所以也能被for....of消费
abc
hello
hy
world
*/
 

讲一下为什么上面可以那样写

因为有一些原生语言特性实现了Iterator接口,只要实现了Iterator(迭代器)接口就能消费Iterable(可迭代对象),上面的for...of它本质上是一个迭代器,所以它可以消费数组(可迭代对象),其实不只for...of,还有以下的原生语言特性实现了Iterator接口

  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收期约组成的可迭代对象
  • Promise.race()接收期约组成的可迭代对象
  • yield*操作符,在生成器中使用

提前终止迭代器

使用迭代器对象的return()方法,可以停止迭代器遍历,但是要注意的是像数组这样的内置对象,即使你执行了return()方法,它也不会让迭代器关闭,而且它还会保留之前遍历的现场,等下次继续开遍历时,会接着上次遍历的地方,继续遍历下去。

下面上代码

let array=[1,2,3,4,5];

let arrayIterator=array[Symbol.iterator]();
arrayIterator.return=function(){
    console.log("停止遍历");
    return {done:true};
}

for(value  of  arrayIterator){
    if(value >2){
        break;
    }
    console.log(value);
}
/*
1
2
停止遍历
*/
console.log(arrayIterator.next());//{ value: 4, done: false }
console.log(arrayIterator.next());//{ value: 5, done: false }
console.log(arrayIterator.next());//{ value: undefined, done: true }
 

自定义一个迭代器

看完上面内置的迭代器后,自己是不是想实现一个迭代器呢^^

现在来说一下,实现一个自定义的迭代器要注意什么!

在迭代器工厂函数里要定义1个变量

  • 变量index:保存迭代器指针的位置,这样下次执行遍历的时候,就会接着这个位置,遍历下一个数据(利用闭包,保存遍历现场)
let obj={
    data:[0,1,3],
    [Symbol.iterator](){
        let index=0;//保存指针的位置
        const self=this;
        return {
            next(){
                if(index<self.data.length)
                {
                    return {value:self.data[index++],done:false};
                }
                else
                    return {value:"undefined",done:true}
            }
        }
    }
}

let objIterator =obj[Symbol.iterator]();
console.log(objIterator.next());//{ value: 0, done: false }
console.log(objIterator.next());//{ value: 1, done: false }
console.log(objIterator.next());//{ value: 3, done: false }
console.log(objIterator.next());//{ value: 'undefined', done: true }
 

iterator

看完图是不是清晰了很多^^

生成器

Generator(生成器) 函数是 ES6 提供的一种异步编程解决方案 ,它是一个状态机,还是一个迭代器生成函数。

调用一个Generator函数,会返回一个迭代器,该迭代器调用next()函数,可以遍历Generator函数中的状态。

Generator函数里,会出现yield关键字,每一个yield关键字就代表Generator函数的一个状态,只要迭代器遍历到yield关键字,生成器函数(Generator)就会中断执行,那什么时候它会再一次执行呢?等到迭代器再次调用next()方法时,生成器函数又会开始执行。

那下面就声明一个简单的Generator函数吧!

function  * createGenerator(){
    console.log("start");
    yield  1;
    yield  2;
    yield  3;
    return "ending";
}


let Generator=createGenerator();//生成了一个迭代器,虽然调用了生成器函数,但是它里面的代码是不会执行的,只有迭代器的next()方法才能驱动生成器函数执行内部的代码。
console.log(Generator.next());
/*
Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象(IteratorResult),它的value属性就是当前yield表达式的值1,done属性的值false,表示遍历还没有结束。
所以在"yield 1"语句前面的"console.log("start")"会执行。
*/
console.log(Generator.next());//{done:false,value:2}
console.log(Generator.next());//{done:false,value:3}
console.log(Generator.next());//{done:true,value:"ending"}
console.log(Generator.next());//{done:true,value:undefined}
console.log(Generator.next());//{done:tre,value:undefined}
 

生成器对象可以作为可迭代对象

废话不多说,直接上代码

function *generator(){
    yield   1;
    yield   2;
    yield   3;
    yield   4;
}

let generatorIterator=generator();//生成器函数返回一个迭代器(生成器对象)
let generatorIteratorIterator=generatorIterator[Symbol.iterator]();
/*
生成器函数返回的迭代器,本质上也是一个可迭代对象,因为它有一个默认迭代器,所以可以用for...of结构来消费生成器对象	
*/
console.log(generatorIteratorIterator)
for(item of generatorIterator){
    console.log(item);
}
/*
1
2
3
4
*/
//和下面的代码等价
console.log(generatorIterator.next().value);//1
console.log(generatorIterator.next().value);//2
console.log(generatorIterator.next().value);//3
console.log(generatorIterator.next().value);//4
 

使用yield实现输入和输出

从阮一峰大神上复制过来的一个例子,我觉得特别好理解

先说明一下,除非下一个next()方法给上一个yield表达式注入参数,否则yield表达式总是返回undefined(这里可能稍微有点难理解,等下我会上图^^)

function* foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var a = foo(5);
console.log(a.next()); // Object{value:6, done:false}
console.log(a.next()); // Object{value:NaN, done:false}
console.log(a.next()); // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
 
  1. 声明了一个变量a,接收生成器foo返回的迭代器。
  2. 执行第一个a.next(),这时候第一个a.next()会输出一个IteratorResult对象包含了value(yield表达式的值)和done(遍历是否完成)两个属性,这里要特别注意的是!!!!变量y的赋值操作被中断了!!!
  3. 执行第二个a.next(),因为第二个a.next()方法没有向生成器函数注入实参,所以上一个yield表达式返回的是undifined,结果y=2*undefined,这就是为什么第二个a.next().value输出的值是NaN,之后变量z的赋值操作被中断。
  4. 执行第三个a.next(),因为第三个a.next()方法没有向生成器函数注入实参,所以上一个yield表达式返回的是undifined,结果z=undefined,这就是为什么第三个a.next().value输出的值是NaN

大家自己来模拟一下吧!

产生可迭代对象

可以使用星号增强yield的行为,让它能够迭代一个可迭代对象,直接上代码

function *Generator(){
    yield *[1,2,3]
}

for(item  of    Generator()){
    console.log(item);
}
//等价
function *Generator(){
    yield   1;
    yield   2;
    yield   3;
}

for(item  of    Generator()){
    console.log(item)
}
 

生成器作为默认迭代器

let obj={
    data:[1,2,3],
    [Symbol.iterator](){
        let index=0;
        let self=this;
        return {
            next(){
                if(index<self.data.length){
                    return{
                        value:self.data[index++],
                        done:false
                    }
                }
                else
                    return{
                        value:undefined,
                        done:true
                    }
            }
        }
    }
}

let obj1= {
    data: [1, 2, 3],
    [Symbol.iterator]:function *Generator(){
        yield *this.data;
    }
}

for(item of obj){
    console.log(item);
}
/*
1
2
3
*/

for(item of obj1){
    console.log(item);
}
/*
1
2
3
*/
 

看完上面的代码是不是觉得让生成器作为默认迭代器很方便!

提前终止生成器

return方法

使用生成器的return()方法可以提前终止生成器,关闭生成器以后,后续调用next()方法会显示done:true状态,而提供的任何返回值都不会被存储或传播,不废话了,直接上代码。

function *generator(){
    yield  1;
    yield  2;
    yield  3;
    yield  4;
    yield  5;
    yield  6;
    yield  7;
    yield  8;
    yield  9;
}

let Iterator=generator();
console.log(Iterator.next());//{ value: 1, done: false }
console.log(Iterator.next());//{ value: 2, done: false }
console.log(Iterator.return("生成器关闭"));//{ value: '生成器关闭', done: true }
console.log(Iterator.next());//{ value: undefined, done: true }
console.log(Iterator.next());//{ value: undefined, done: true }
console.log(Iterator.next());//{ value: undefined, done: true }
console.log(Iterator.next());//{ value: undefined, done: true }
 

throw方法

throw()方法会再暂停的时候将一个提供的错误注入到生成器对象中。如果错误未处理,生成器就会关闭,直接上代码。

function *generator(){
    yield  1;
    yield  2;
    yield  3;
    yield  4;
    yield  5;
    yield  6;
    yield  7;
    yield  8;
    yield  9;
}

let Iterator=generator();
try {
    console.log(Iterator.next());
    console.log(Iterator.next());
    console.log(Iterator.throw("生成器关闭"));
    console.log(Iterator.next());
    console.log(Iterator.next());
    console.log(Iterator.next());
    console.log(Iterator.next());
}catch(e){
    console.log(e);
}
/*
{ value: 1, done: false }
{ value: 2, done: false }
生成器关闭
*/

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
function *generator(){
    try{
        yield  1;
        yield  2;
        yield  3;
        yield  4;
        yield  5;
        yield  6;
        yield  7;
        yield  8;
        yield  9;
    }catch(e){
        console.log(e);
    }
}

let Iterator=generator();
try {
    console.log(Iterator.next());
    console.log(Iterator.next());
    console.log(Iterator.throw("生成器关闭"));
    console.log(Iterator.next());
    console.log(Iterator.next());
    console.log(Iterator.next());
    console.log(Iterator.next());
}catch(e){
    console.log(e);
}
/*
{ value: 1, done: false }
{ value: 2, done: false }
生成器关闭
{ value: undefined, done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }
*/
 

到这里生成器的内容就差不多了,下面来说一说生成器的一些应用场景

应用场景

协程场景

什么是协程?

我这里就不多说了,可以参考一下廖雪峰老师的文章,直接上代码

function  cookTask(wash,cook){
    let washIterator=wash();
    let cookIterator=cook();
    console.log(washIterator.next().value);
    console.log(cookIterator.next().value);
    console.log(washIterator.next().value);
    console.log(cookIterator.next().value);
    console.log(washIterator.next().value);
    console.log(cookIterator.next().value);
}
function  *wash(){
    yield "我洗好了白菜";
    yield "我洗好了猪肉";
    yield "我洗好了鸡翅";
}

function  *cook(){
    yield  '我煮好了白菜';
    yield  '我煮好了猪肉';
    yield  '我煮好了鸡翅';
}

cookTask(wash,cook);
/*
我洗好了白菜
我煮好了白菜
我洗好了猪肉
我煮好了猪肉
我洗好了鸡翅
我煮好了鸡翅
*/
 

我们这里模拟了炒菜的场景,这里有一个机器人厨师,首先这个机器人会执行洗菜的功能,当洗菜功能完成了,就将控制权让出给煮菜的功能,当煮菜的功能结束了,就将控制权让出给洗菜的功能。

异步操作同步化

这是阮一峰大神的一个例子

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

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();
 

说一下思路

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

用Generator函数把异步任务同步化,是不是很神奇^^

结尾

这篇文章到这里就结束了,以上如果有什么内容是错误的,希望大佬们能指出^^

回复

我来回复
  • 暂无回复内容