深入理解ES6--8.迭代器与生成器

原创文章&经验总结&从校招到A厂一路阳光一路沧桑

详情请戳www.codercc.com

image

主要知识点:迭代器、生成器、可迭代对象以及for-of循环、迭代器的高级功能以及创建异步任务处理器

迭代器与生成器.png

1. 迭代器

何为迭代器?

迭代器是被设计专用于迭代的对象,带有特定接口。所有的迭代器对象都拥有 next() 方
法,会返回一个结果对象。该结果对象有两个属性:对应下一个值的 value ,以及一个布尔
类型的 done ,其值为 true 时表示没有更多值可供使用。迭代器持有一个指向集合位置的
内部指针,每当调用了 next() 方法,迭代器就会返回相应的下一个值。

2. 生成器

何为生成器?

生成器(generator ) 是能返回一个迭代器的函数。生成器函数由放在 function 关键字之后的一个星号( * ) 来表示,并能使用新的 yield 关键字。将星号紧跟在 function 关键字之后,或是在中间留出空格,都是没问题的。例如:

function*generator(){

    yield 1;
    yield 2;
    yield 3;
}

let iterator = generator();
console.log(iterator.next().value);//1
console.log(iterator.next().value);//2

生成器函数最有意思的地方是它们会在每一个yield语句后停止,例如在上面的代码中执行yield 1后,该函数不会在继续往下执行。等待下一次调用next()后,才会继续往下执行yield 2

除了使用函数声明的方式创建一个生成器外,还可以使用函数表达式来创建一个生成器。由于生成器就是一个函数,同样可以使用对象字面量的方式,将对象的属性赋值为一个生成器函数。

3. 可迭代对象与for-of循环

可迭代对象是包含Symbol.iterator属性的对象,这个Symbol.iterator属性对应着能够返回该对象的迭代器的函数。在ES6中,所有的集合对象(数组、Set和Map)以及字符串都是可迭代对象,因此它们都被指定了默认的迭代器。可迭代对象可以与ES6中新增的for-of循环配合使用。

迭代器解决了for循环中追踪索引的问题,而for-of循环,则是完全删除追踪集合索引的需要,更能专注于操作集合内容。for-of循环在循环每次执行时会调用可迭代对象的next()方法,并将结果对象的value值存储在一个变量上,循环过程直到结果对象done属性变成true为止:

let arr = [1,2,3];
for(let num of arr){
    console.log(num);
}
输出结果为:1,2,3

for-of循环首先会调用arr数组中Symbol.iterator属性对象的函数,就会获取到该数组对应的迭代器,接下来iterator.next()被调用,迭代器结果对象的value属性会被放入到变量num中。数组中的数据项会依次存入到变量num中,直到迭代器结果对象中的done属性变成true为止,循环就结束。

访问可迭代对象的默认迭代器

可以使用可迭代对象的Symbol.iterator来访问对象上可返回迭代器的函数:

let arr = [1,2,3];
//访问默认迭代器
let iterator = arr[Symbol.iterator]();
console.log(iterator.next().value); //1
console.log(iterator.next().value); //2

通过Symbol.iterator属性获取到该对象的可返回迭代器的函数,然后执行该函数得到对象的可迭代器。同样的,可是使用Symbol.iterator属性来检查对象是否是可迭代对象。

创建可迭代对象

数组,Set等集合对象是默认的迭代器,当然也可以为对象创建自定义的迭代器,使其成为可迭代对象。那么迭代器如何生成?我们已经知道,生成器就是一个可以返回迭代器的函数,因此自定义迭代器,就是写一个生成器函数。同时,可迭代对象必须具有Symbol.iterator属性,并且该属性对应着一个能够返回迭代器的函数,因此只需要将这个生成器函数赋值给Symbol.iterator属性即可:

//创建可迭代对象

let obj = {
    items:[],

    *[Symbol.iterator](){
        for(let item of this.items){
            yield item;
        }
    }
}

obj.items.push(1);
obj.items.push(2);

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

输出:1,2

4. 内置的迭代器

ES6中许多内置类型已经包含了默认的迭代器,只有当默认迭代器满足不了时,才会创建自定义的迭代器。如果新建对象时,要想把该对象转换成可迭代对象的话,一般才会需要自定义迭代器。

集合迭代器

ES6中有三种集合对象:数组、Map和Set,这三种类型都拥有默认的迭代器:

  • entries():返回一个包含键值对的迭代器;
  • values():返回一个包含集合中的值的迭代器;
  • keys():返回一个包含集合中的键的迭代器;
  1. 调用entries()迭代器会在每次调用next()方法返回一个双项数组,此数组代表集合数据项中的键和值:对于数组来说,第一项是数组索引;对于Set来说,第一项是值(因为Set的键和值相同),对于Map来说,就是键值对的值;
  2. values()迭代器能够返回集合中的每一个值;
  3. keys()迭代器能够返回集合中的每一个键;

集合的默认迭代器

当for-of循环没有显式指定迭代器时,集合对象会有默认的迭代器。values()方法是数组和Set默认的迭代器,而entries()方法是Map默认迭代器。

字符串的迭代器

ES6旨在为Unicode提供了完全支持,字符串的默认迭代器就是解决字符串迭代问题的一种尝试,这样一来,借助字符串默认迭代器就能处理字符而非码元:

//字符串默认迭代器
let str ='A   B';
for(let s of str){
    console.log(s); //A  B
}

扩展运算符与非数组的可迭代对象

扩展运算符能作用于所有可迭代对象,并且会使用默认迭代器来判断需要哪些值。在数组字面量中可以使用扩展运算符将可迭代对象填充到数组中:

//扩展运算符可作用到所有可迭代对象
let arr = [1,2,3];
let array = [...arr];
console.log(array); [1,2,3]

并且,可以不限次数在数组字面量中使用扩展运算符,而且可以在任意位置用扩展运算符将可迭代对象填充到数组中:

let arr = [1,2,3];
let arr2 = [7,8,9];
let array = [...arr,5,...arr2];
console.log(array); //1,2,3,5,7,8,9

5. 迭代器高级功能

能够通过next()方法向迭代器传递参数,当一个参数传递给next()方法时,该参数就会成为生成器内部yield语句中的变量值。

//迭代器的高级功能
function * generator(){
    let first = yield 1;
    let second = yield first+2;
    let third = yield second+3;
}

let iterator = generator();
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()方法传入参数时,会赋值给yield语句中的变量。

在迭代器中抛出错误

能传递给迭代器的不仅是数据,还可以是错误,迭代器可以选择一个throw()方法,用于指示迭代器应在恢复执行时抛出一个错误:

//迭代器抛出错误

function * generator(){
    let first = yield 1;        
    let second = yield first+2;     
    let third = yield second+3;
}


let iterator = generator();
console.log(iterator.next()); //{value: 1, done: false}
console.log(iterator.next(4)); //{value: 6, done: false}
console.log(iterator.throw(new Error('Error!'))); //Uncaught Error: Error!
console.log(iterator.next()); //不会执行

在生成器中同样可以使用try-catch来捕捉错误:

function * generator(){
    let first = yield 1;
    let second;
    try{
        second = yield first+2;
    }catch(ex){
        second = 6
    }   
    let third = yield second+3;
}


let iterator = generator();
console.log(iterator.next()); //{value: 1, done: false}
console.log(iterator.next(4)); //{value: 6, done: false}
console.log(iterator.throw(new Error('Error!'))); //{value: 9, done: false}
console.log(iterator.next()); //{value: undefined, done: true}

生成器的return语句

由于生成器是函数,你可以在它内部使用 return 语句,既可以让生成器早一点退出执行,也可以指定在 next() 方法最后一次调用时的返回值。大多数情况,迭代器上的
next() 的最后一次调用都返回了 undefined ,但你还可以像在其他函数中那样,使用
return 来指定另一个返回值。在生成器内, return 表明所有的处理已完成,因此 done
属性会被设为 true ,而如果提供了返回值,就会被用于 value 字段。比如,利用return让生成器更早的退出:

function * gene(){
    yield 1;
    return;
    yield 2;
    yield 3;
}

let iterator = gene();
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: undefined, done: true}
console.log(iterator.next());//{value: undefined, done: true}

由于使用return语句,能够让生成器更早结束,因此在第二次以及第三次调用next()方法时,返回结果对象为:{value: undefined, done: true}

还可以使用return语句指定最后返回值:

function * gene(){
    yield 1;
    return 'finish';
}

let iterator = gene();
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: "finish", done: true}
console.log(iterator.next());//{value: undefined, done: true}

当第二次调用next()方法时,返回了设置的返回值:finish。第三次调用 next() 返回了一个对象,其 value 属性再次变回undefined ,你在 return 语句中指定的任意值都只会在结果对象中出现一次,此后 value 字段就会被重置为 undefined

生成器委托

生成器委托是指:将生成器组合起来使用,构成一个生成器。组合生成器的语法需要yield**落在yield关键字与生成器函数名之间即可:

function * gene1(){
    yield 'red';
    yield 'green';

}
function * gene2(){
    yield 1;
    yield 2;
}

function * combined(){
    yield * gene1();
    yield * gene2();
}

let iterator = combined();
console.log(iterator.next());//{value: "red", done: false}
console.log(iterator.next());//{value: "green", done: false}
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: 2, done: true}
console.log(iterator.next());//{value: undefined, done: true}

此例中将生成器gene1和gene2组合而成生成器combined,每次调用combined的next()方法时,实际上会委托到具体的生成器中,当gene1生成器中所有的yield执行完退出之后,才会继续执行gene2,当gene2执行完退出之后,也就意味着combined生成器执行结束。

在使用生成器委托组合新的生成器时,前一个执行的生成器返回值可以作为下一个生成器的参数:

//利用生成器返回值

function * gene1(){
    yield 1;
    return 2;
}

function * gene2(count){

    for(let i=0;i<count;i++){
        yield 'repeat';
    }
}

function * combined(){
    let result = yield * gene1();
    yield result;
    yield*gene2(result);
}
let iterator = combined();
console.log(iterator.next());//{value: 1, done: false}
console.log(iterator.next());//{value: 2, done: false}
console.log(iterator.next());//{value: "repeat", done: false}
console.log(iterator.next());//{value: "repeat", done: false}
console.log(iterator.next());//{value: undefined, done: true}

此例中,生成器gene1的返回值,就作为了生成器gene2的参数。

6. 异步任务

一个简单的任务运行器

生成器函数中yield能暂停运行,当再次调用next()方法时才会重新往下运行。一个简单的任务执行器,就需要传入一个生成器函数,然后每一次调用next()方法就会“一步步”往下执行函数:

//任务执行器
function run(taskDef) {
    // 创建迭代器,让它在别处可用
    let task = taskDef();
    // 启动任务
    let result = task.next();
    // 递归使用函数来保持对 next() 的调用
    function step() {
    // 如果还有更多要做的
    if (!result.done) {
        result = task.next();
        step();
    }
    } 
    // 开始处理过程
    step();
}


run(function*() {
    console.log(1);
    yield;
    console.log(2);
    yield;
    console.log(3);
});

run() 函数接受一个任务定义(即一个生成器函数) 作为参数,它会调用生成器来创建一个
迭代器,并将迭代器存放在 task 变量上。第一次对 next() 的调用启动
了迭代器,并将结果存储下来以便稍后使用。step() 函数查看result.done 是否为 false,如果是就在递归调用自身之前调用 next() 方法。每次调用 next() 都会把返回的结果保
存在 result 变量上,它总是会被最新的信息所重写。对于 step() 的初始调用启动了处理
过程,该过程会查看 result.done 来判断是否还有更多要做的工作。

能够传递数据的任务运行器

如果需要传递数据的话,也很容易,也就是将上一次yield的值,传递给下一次next()调用即可,仅仅只需要传送结果对象的value属性:

//任务执行器
function run(taskDef) {
    // 创建迭代器,让它在别处可用
    let task = taskDef();
    // 启动任务
    let result = task.next();
    // 递归使用函数来保持对 next() 的调用
    function step() {
        // 如果还有更多要做的
        if (!result.done) {
            result = task.next(result.value);               
            console.log(result.value); //6 undefined
            step();
        }
    } 
    // 开始处理过程
    step();
}


run(function*() {
    let value = yield 1;
    yield value+5;
});

异步任务

上面的例子是简单的任务处理器,甚至还是同步的。实现任务器也主要是迭代器在每一次调用next()方法时彼此间传递静态参数。如果要将上面的任务处理器改装成异步任务处理器的话,就需要yield能够返回一个能够执行回调函数的函数,并且回调参数为该函数的参数即可。

什么是有回调函数的函数?

有这样的示例代码:

function fetchData(callback) {
    return function(callback) {
        callback(null, "Hi!");
    };
}

函数fetchData返回的是一个函数,并且所返回的函数能够接受一个函数callback。当执行返回的函数时,实际上是调用回调函数callback。但目前而言,回调函数callback还是同步的,可以改造成异步函数:

function fetchData(callback) {
    return function(callback) {
            setTimeout(function() {
                callback(null, "Hi!");
        }, 50);
    };
}

一个简单的异步任务处理器:

//异步任务处理器

function run(taskDef){

    //执行生成器,创建迭代器
    let task = taskDef();
    //启动任务
    let result = task.next();

    function step(){
        while(!result.done){
            if(typeof(result.value)==='function' ){
                result.value(()=>{
                    console.log('hello world');
                })
            }                   
            result = task.next();
            step();
        }           
    }
    step();
}




run(function *(){
    //返回一个能够返回执行回调函数的函数,并且回调函数还是该
    //函数的参数
    yield function(callback){
        setTimeout(callback,3000);
    }
});

上面的示例代码就是一个简单的异步任务处理器,有这样几点要点:

  1. 使用生成器构造迭代器,所以在run方法中传入的是生成器函数;
  2. 生成器函数中yield关键字,返回的是一个能够执行回调函数的函数,并且回调函数是该函数的一个参数

7. 总结

  1. 使用迭代器可以用来遍历集合对象包含的数据,调用迭代器的next()方法可以返回一个结果对象,其中value属性代表值,done属性用来表示集合对象是否已经到了最后一项,如果集合对象的值全部遍历完后,done属性为true

  2. Symbol.iterator属性被用于定义对象的默认迭代器,使用该属性可以为对象自定义迭代器。当Symbol.iterator属性存在时,该对象可以被认为是可迭代对象;

  3. 可迭代对象可以使用for-of循环,for-of循环不需要关注集合对象的索引,更能专注于对内容的处理;

  4. 数组、Set、Map以及字符串都具有默认的迭代器;

  5. 扩展运算符可以作用于任何可迭代对象,让可迭代对象转换成数组,并且扩展运算符可以用于数组字面量中任何位置中,让可迭代对象的数据项一次填入到新数组中;

  6. 生成器是一个特殊的函数,语法上使用了*,yield能够返回结果,并能暂停继续往下执行,直到调用next()方法后,才能继续往下执行。使用生成器委托能够将两个生成器合并组合成一个生成器;

  7. 能够使用生成器构造异步任务处理器;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容