资讯专栏INFORMATION COLUMN

【重温基础】13.迭代器和生成器

ymyang / 1065人阅读

摘要:迭代器和生成器将迭代的概念直接带入核心语言,并提供一种机制来自定义循环的行为。本文主要会介绍中新增的迭代器和生成器。属性本身是函数,是当前数据结构默认的迭代器生成函数。

本文是 重温基础 系列文章的第十三篇。
今日感受:每次自我年终总结,都会有各种情绪和收获。

系列目录:

【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)

【重温基础】1.语法和数据类型

【重温基础】2.流程控制和错误处理

【重温基础】3.循环和迭代

【重温基础】4.函数

【重温基础】5.表达式和运算符

【重温基础】6.数字

【重温基础】7.时间对象

【重温基础】8.字符串

【重温基础】9.正则表达式

【重温基础】10.数组

【重温基础】11.Map和Set对象

【重温基础】12.使用对象

本章节复习的是JS中的迭代器和生成器,常常用来处理集合。

前置知识:
JavaScrip已经提供多个迭代集合的方法,从简单的for循环到map()filter()
迭代器和生成器将迭代的概念直接带入核心语言,并提供一种机制来自定义for...of循环的行为。

本文会将知识点分为两大部分,简单介绍和详细介绍
简单介绍,适合基础入门会使用的目标;
详细介绍,会更加深入的做介绍,适合理解原理;

1. 概述

当我们使用循环语句迭代数据时,需初始化一个变量来记录每一次迭代在数据集合中的位置:

let a = ["aaa","bbb","ccc"];
for (let i = 0; i< a.length; i++){
    console.log(a[i]);
}

这边的i就是我们用来记录迭代位置的变量,但是在ES6开始,JavaScrip引入了迭代器这个特性,并且新的数组方法新的集合类型(如Set集合Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的for-of循环、展开运算符(...),甚至连异步编程都可以使用迭代器。

本文主要会介绍ES6中新增的迭代器(Iterator)和生成器(Generator)。

2. 迭代器(简单介绍)

迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都会返回一个结果对象。
这个结果对象,有两个属性:

value: 表示下一个将要返回的值。

done: 一个布尔值,若没有更多可返回的数据时,值为true,否则false

如果最后一个值返回后,再调用next(),则返回的对象的done值为true,而value值如果没有值的话,返回的为undefined

ES5实现一个迭代器:

function myIterator(list){
    var i = 0;
    return {
        next: function(){
            var done = i >= list.length;
            var value = !done ? list[i++] : undefined;
            return {
                done : done,
                value : value
            }
        }
    }
}

var iterator = myIterator([1,2,3]);
iterator.next();  // "{done: false, value: 1}"
iterator.next();  // "{done: false, value: 2}"
iterator.next();  // "{done: false, value: 3}"
iterator.next();  // "{done: true, value: undefined}"
// 以后的调用都一样
iterator.next();  // "{done: true, value: undefined}"

从上面代码可以看出,ES5的实现还是比较麻烦,而ES6新增的生成器,可以使得创建迭代器对象的过程更加简单。

3. 生成器(简单介绍)

生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格。

function *myIterator(){
    yield 1;
    yield 2;
    yield 3;
}
let iterator = myIterator();
iterator.next();  // "{done: false, value: 1}"
iterator.next();  // "{done: false, value: 2}"
iterator.next();  // "{done: false, value: 3}"
iterator.next();  // "{done: true, value: undefined}"
// 以后的调用都一样
iterator.next();  // "{done: true, value: undefined}"

生成器函数最有趣的部分是,每当执行完一条yield语句后函数就会自动停止执行,比如上面代码,当yield 1;执行完后,便不会执行任何语句,而是等到再调用迭代器的next()方法才会执行下一个语句,即yield 2;.
使用yield关键字可以返回任何值和表达式,因为可以通过生成器函数批量给迭代器添加元素:

function *myIterator(list){
    for(let  i = 0; i< list.length ; i ++){
        yield list[i];
    }
}

var iterator = myIterator([1,2,3]);
iterator.next();  // "{done: false, value: 1}"
iterator.next();  // "{done: false, value: 2}"
iterator.next();  // "{done: false, value: 3}"
iterator.next();  // "{done: true, value: undefined}"
// 以后的调用都一样
iterator.next();  // "{done: true, value: undefined}"

生成器的适用返回很广,可以将它用于所有支持函数使用的地方。

4. 迭代器(详细介绍) 4.1 Iterator迭代器概念
Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成迭代操作(即依次处理该数据结构的所有成员)。

Iterator三个作用

为各种数据结构,提供一个统一的、简便的访问接口;

使得数据结构的成员能够按某种次序排列;

Iterator 接口主要供ES6新增的for...of消费;

4.2 Iterator迭代过程

创建一个指针对象,指向当前数据结构的起始位置。也就是说,迭代器对象本质上,就是一个指针对象。

第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。

value属性是当前成员的值;

done属性是一个布尔值,表示迭代是否结束;

模拟next方法返回值:

let f = function (arr){
    var nextIndex = 0;
    return {
        next:function(){
            return nextIndex < arr.length ?
            {value: arr[nextIndex++], done: false}:
            {value: undefined, done: true}
        }
    }
}

let a = f(["a", "b"]);
a.next(); // { value: "a", done: false }
a.next(); // { value: "b", done: false }
a.next(); // { value: undefined, done: true }
4.3 默认Iterator接口

若数据可迭代,即一种数据部署了Iterator接口。
ES6中默认的Iterator接口部署在数据结构的Symbol.iterator属性,即如果一个数据结构具有Symbol.iterator属性,就可以认为是可迭代
Symbol.iterator属性本身是函数,是当前数据结构默认的迭代器生成函数。执行这个函数,就会返回一个迭代器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。

原生具有Iterator接口的数据结构有

Array

Map

Set

String

TypedArray

函数的 arguments 对象

NodeList 对象

4.4 Iterator使用场景

(1)解构赋值

对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。

let a = new Set().add("a").add("b").add("c");
let [x, y] = a;       // x = "a"  y = "b"
let [a1, ...a2] = a;  // a1 = "a" a2 = ["b","c"]

(2)扩展运算符

扩展运算符(...)也会调用默认的 Iterator 接口。

let a = "hello";
[...a];            //  ["h","e","l","l","o"]

let a = ["b", "c"];
["a", ...a, "d"];  // ["a", "b", "c", "d"]

(2)yield*

yield*后面跟的是一个可迭代的结构,它会调用该结构的迭代器接口。

let a = function*(){
    yield 1;
    yield* [2,3,4];
    yield 5;
}

let b = a();
b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // { value: 4, done: false }
b.next() // { value: 5, done: false }
b.next() // { value: undefined, done: true }

(4)其他场合

由于数组的迭代会调用迭代器接口,所以任何接受数组作为参数的场合,其实都调用了迭代器接口。下面是一些例子。

for...of

Array.from()

Map(), Set(), WeakMap(), WeakSet()(比如new Map([["a",1],["b",2]])

Promise.all()

Promise.race()

4.5 for...of循环

只要数据结构部署了Symbol.iterator属性,即具有 iterator 接口,可以用for...of循环迭代它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterato方法。
使用场景
for...of可以使用在数组SetMap结构类数组对象Genetator对象字符串

数组

for...of循环可以代替数组实例的forEach方法。

let a = ["a", "b", "c"];
for (let k of a){console.log(k)}; // a b c

a.forEach((ele, index)=>{
    console.log(ele);    // a b c
    console.log(index);  // 0 1 2 
})

for...in对比,for...in只能获取对象键名,不能直接获取键值,而for...of允许直接获取键值。

let a = ["a", "b", "c"];
for (let k of a){console.log(k)};  // a b c
for (let k in a){console.log(k)};  // 0 1 2

Set和Map

可以使用数组作为变量,如for (let [k,v] of b){...}

let a = new Set(["a", "b", "c"]);
for (let k of a){console.log(k)}; // a b c

let b = new Map();
b.set("name","leo");
b.set("age", 18);
b.set("aaa","bbb");
for (let [k,v] of b){console.log(k + ":" + v)};
// name:leo
// age:18
// aaa:bbb

类数组对象

// 字符串
let a = "hello";
for (let k of a ){console.log(k)}; // h e l l o

// DOM NodeList对象
let b = document.querySelectorAll("p");
for (let k of b ){
    k.classList.add("test");
}

// arguments对象
function f(){
    for (let k of arguments){
        console.log(k);
    }
}
f("a","b"); // a b

对象

普通对象不能直接使用for...of会报错,要部署Iterator才能使用。

let a = {a:"aa",b:"bb",c:"cc"};
for (let k in a){console.log(k)}; // a b c
for (let k of a){console>log(k)}; // TypeError
4.6 跳出for...of

使用break来实现。

for (let k of a){
    if(k>100)
        break;
    console.log(k);
}
5. 生成器(详细介绍) 5.1 基本概念

Generator生成器函数是一种异步编程解决方案。
原理
执行Genenrator函数会返回一个遍历器对象,依次遍历Generator函数内部的每一个状态。
Generator函数是一个普通函数,有以下两个特征:

function关键字与函数名之间有个星号;

函数体内使用yield表达式,定义不同状态;

通过调用next方法,将指针移向下一个状态,直到遇到下一个yield表达式(或return语句)为止。简单理解,Generator函数分段执行,yield表达式是暂停执行的标记,而next恢复执行。

function * f (){
    yield "hi";
    yield "leo";
    return "ending";
}
let a = f();
a.next();  // {value: "hi", done : false}
a.next();  // {value: "leo", done : false}
a.next();  // {value: "ending", done : true}
a.next();  // {value: undefined, done : false}
5.2 yield表达式

yield表达式是暂停标志,遍历器对象的next方法的运行逻辑如下:

遇到yield就暂停执行,将这个yield后的表达式的值,作为返回对象的value属性值。

下次调用next往下执行,直到遇到下一个yield

直到函数结束或者return为止,并返回return语句后面表达式的值,作为返回对象的value属性值。

如果该函数没有return语句,则返回对象的valueundefined

注意:

yield只能用在Generator函数里使用,其他地方使用会报错。

// 错误1
(function(){
    yiled 1;  // SyntaxError: Unexpected number
})()

// 错误2  forEach参数是个普通函数
let a = [1, [[2, 3], 4], [5, 6]];
let f = function * (i){
    i.forEach(function(m){
        if(typeof m !== "number"){
            yield * f (m);
        }else{
            yield m;
        }
    })
}
for (let k of f(a)){
    console.log(k)
}

yield表达式如果用于另一个表达式之中,必须放在圆括号内。

function * a (){
    console.log("a" + yield);     //  SyntaxErro
    console.log("a" + yield 123); //  SyntaxErro
    console.log("a" + (yield));     //  ok
    console.log("a" + (yield 123)); //  ok
}

yield表达式用做函数参数或放在表达式右边,可以不加括号

function * a (){
    f(yield "a", yield "b");    //  ok
    lei i = yield;              //  ok
}
5.3 next方法

yield本身没有返回值,或者是总返回undefinednext方法可带一个参数,作为上一个yield表达式的返回值。

function * f (){
    for (let k = 0; true; k++){
        let a = yield k;
        if(a){k = -1};
    }
}
let g =f();
g.next();    // {value: 0, done: false}
g.next();    // {value: 1, done: false}
g.next(true);    // {value: 0, done: false}

这一特点,可以让Generator函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。

function * f(x){
    let y = 2 * (yield (x+1));
    let z = yield (y/3);
    return (x + y + z);
}
let a = f(5);
a.next();   // {value : 6 ,done : false}
a.next();   // {value : NaN ,done : false}  
a.next();   // {value : NaN ,done : true}
// NaN因为yeild返回的是对象 和数字计算会NaN

let b = f(5);
b.next();     // {value : 6 ,done : false}
b.next(12);   // {value : 8 ,done : false}
b.next(13);   // {value : 42 ,done : false}
// x 5 y 24 z 13
5.4 for...of循环

for...of循环会自动遍历,不用调用next方法,需要注意的是,for...of遇到next返回值的done属性为true就会终止,return返回的不包括在for...of循环中。

function * f(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    return 5;
}
for (let k of f()){
    console.log(k);
}
// 1 2 3 4  没有 5 
5.5 Generator.prototype.throw()

throw方法用来向函数外抛出错误,并且在Generator函数体内捕获。

let f = function * (){
    try { yield }
    catch (e) { console.log("内部捕获", e) }
}

let a = f();
a.next();

try{
    a.throw("a");
    a.throw("b");
}catch(e){
    console.log("外部捕获",e);
}
// 内部捕获 a
// 外部捕获 b
5.6 Generator.prototype.return()

return方法用来返回给定的值,并结束遍历Generator函数,如果return方法没有参数,则返回值的value属性为undefined

function * f(){
    yield 1;
    yield 2;
    yield 3;
}
let g = f();
g.next();          // {value : 1, done : false}
g.return("leo");   // {value : "leo", done " true}
g.next();          // {value : undefined, done : true}
5.7 next()/throw()/return()共同点

相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换yield表达式。

next()yield表达式替换成一个值。

let f = function * (x,y){
    let r = yield x + y;
    return r;
}
let g = f(1, 2); 
g.next();   // {value : 3, done : false}
g.next(1);  // {value : 1, done : true}
// 相当于把 let r = yield x + y;
// 替换成 let r = 1;

throw()yield表达式替换成一个throw语句。

g.throw(new Error("报错"));  // Uncaught Error:报错
// 相当于将 let r = yield x + y
// 替换成 let r = throw(new Error("报错"));

next()yield表达式替换成一个return语句。

g.return(2); // {value: 2, done: true}
// 相当于将 let r = yield x + y
// 替换成 let r = return 2;
5.8 yield* 表达式

用于在一个Generator中执行另一个Generator函数,如果没有使用yield*会没有效果。

function * a(){
    yield 1;
    yield 2;
}
function * b(){
    yield 3;
    yield * a();
    yield 4;
}
// 等同于
function * b(){
    yield 3;
    yield 1;
    yield 2;
    yield 4;
}
for(let k of b()){console.log(k)}
// 3
// 1
// 2
// 4
5.9 应用场景

控制流管理

解决回调地狱:

// 使用前
f1(function(v1){
    f2(function(v2){
        f3(function(v3){
            // ... more and more
        })
    })
})

// 使用Promise 
Promise.resolve(f1)
    .then(f2)
    .then(f3)
    .then(function(v4){
        // ...
    },function (err){
        // ...
    }).done();

// 使用Generator
function * f (v1){
    try{
        let v2 = yield f1(v1);
        let v3 = yield f1(v2);
        let v4 = yield f1(v3);
        // ...
    }catch(err){
        // console.log(err)
    }
}
function g (task){
    let obj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if(!obj.done){
      task.value = obj.value;
      g(task);
  }
}
g( f(initValue) );

异步编程的使用

在真实的异步任务封装的情况:

let fetch = require("node-fetch");
function * f(){
    let url = "http://www.baidu.com";
    let res = yield fetch(url);
    console.log(res.bio);
}
// 执行该函数
let g = f();
let result = g.next();
// 由于fetch返回的是Promise对象,所以用then
result.value.then(function(data){
    return data.json();
}).then(function(data){
    g.next(data);
})
参考资料

1.MDN 迭代器和生成器
2.ES6中的迭代器(Iterator)和生成器(Generator)

本部分内容到这结束

Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推荐 https://github.com/pingan8787...
JS小册 js.pingan8787.com

欢迎关注微信公众号【前端自习课】每天早晨,与您一起学习一篇优秀的前端技术博文 .

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100747.html

相关文章

  • 重温基础】14.元编程

    摘要:本文是重温基础系列文章的第十四篇。元,是指程序本身。有理解不到位,还请指点,具体详细的介绍,可以查看维基百科元编程。拦截,返回一个布尔值。 本文是 重温基础 系列文章的第十四篇。 这是第一个基础系列的最后一篇,后面会开始复习一些中级的知识了,欢迎持续关注呀! 接下来会统一整理到我的【Cute-JavaScript】的JavaScript基础系列中。 今日感受:独乐乐不如众乐乐...

    cc17 评论0 收藏0
  • 重温基础】15.JS对象介绍

    摘要:构造函数通常首字母大写,用于区分普通函数。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。中所有的对象,都有一个属性,指向实例对象的构造函数原型由于是个非标准属性,因此只有和两个浏览器支持,标准方法是。 从这篇文章开始,复习 MDN 中级教程 的内容了,在初级教程中,我和大家分享了一些比较简单基础的知识点,并放在我的 【Cute-JavaScript】系...

    booster 评论0 收藏0
  • 重温基础】10.数组

    摘要:本文是重温基础系列文章的第十篇。返回一个由回调函数的返回值组成的新数组。返回一个数组迭代器对象,该迭代器会包含所有数组元素的键值对。回调函数接收三个参数,当前值当前位置和原数组。 本文是 重温基础 系列文章的第十篇。 今日感受:平安夜,多棒。 系列目录: 【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理) 【重温基础】1.语法和数据类型 【重温基础】2.流程控制和错误...

    DangoSky 评论0 收藏0
  • 重温基础】3.循环和迭代

    摘要:本文是重温基础系列文章的第三篇,今天想起鬼脚七的一句话人不一定自由,但思想一定是自由的。系列目录复习资料资料整理个人整理重温基础语法和数据类型重温基础流程控制和错误处理本章节复习的是中的循环语句,让我们能更快速且简单的完成一些需求。 本文是 重温基础 系列文章的第三篇,今天想起鬼脚七的一句话:人不一定自由,但思想一定是自由的。思想没有对和错,也没有高和低,只有不同。了解一个人可以去了解...

    miracledan 评论0 收藏0
  • python奇遇记:迭代器和成器

    摘要:来说说迭代器和生成器,还有可迭代对象和生成器表达式。有点绕是不是,其实,一般只要知道可迭代对象以及它是如何实现的就行了,中常常用生成器来代替迭代器,可以说,生成器就是迭代器。 来说说迭代器和生成器,还有可迭代对象和生成器表达式。 之前简单的提到过,一个对象是可迭代的可以理解为能够使用for循环。这样说其实不太准确,某个对象可迭代是因为它内部实现了$__iter__$这个特殊方法。比如在...

    atinosun 评论0 收藏0

发表评论

0条评论

ymyang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<