资讯专栏INFORMATION COLUMN

ES6系列---迭代器(Iterator)与生成器(Generator)

DrizzleX / 970人阅读

摘要:迭代器的出现旨在消除这种复杂性并减少循环中的错误。返回一个迭代器,其值为集合的值。在迭代器中抛出错误除了给迭代器传递数据外,还可以给它传递错误条件。通过方法,当迭代器恢复执行时可令其抛出一个错误。

循环语句的问题
var colors = ["red", "green", "blue"];
for(var i=0; i

ES6之前,这种标准的for循环,通过变量来跟踪数组的索引。如果多个循环嵌套就需要追踪多个变量,代码复杂度会大大增加,也容易产生错用循环变量的bug。

迭代器的出现旨在消除这种复杂性并减少循环中的错误。

什么是迭代器

我们先感受一下用ES5语法模拟创建一个迭代器:

function createIterator(items) {
    var i = 0;
    
    return { // 返回一个迭代器对象
        next: function() { // 迭代器对象一定有个next()方法
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;
            
            return { // next()方法返回结果对象
                value: value,
                done: done
            };
        }
    };
}

var iterator = createIterator([1, 2, 3]);

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: undefiend, done: true}"
// 之后所有的调用都会返回相同内容
console.log(iterator.next());  // "{ value: undefiend, done: true}"

以上,我们通过调用createIterator()函数,返回一个对象,这个对象存在一个next()方法,当next()方法被调用时,返回格式{ value: 1, done: false}的结果对象。
因此,我们可以这么定义:迭代器是一个拥有next()方法的特殊对象,每次调用next()都返回一个结果对象。

借助这个迭代器对象,我们来改造刚开始那个标准的for循环【暂时先忘记ES6的for-of循环新特性】:

var colors = ["red", "green", "blue"];
var iterator = createIterator(colors);
while(!iterator.next().done){
    console.log(iterator.next().value);
}

what?,消除循环变量而已,需要搞这么麻烦,代码上不是得不偿失了吗?
并非如此,毕竟createIterator()只需写一次,就可以一直复用。不过ES6引入了生成器对象,可以让创建迭代器的过程变得更加简单。

什么是生成器

生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield

function *createIterator(items) {
    for(let i=0; i

上面,我们用ES6的生成器,大大简化了迭代器的创建过程。我们给生成器函数createIterator()传入一个items数组,函数内部,for循环不断从数组中生成新的元素放入迭代器中,每遇到一个yield语句循环都会停止;每次调用迭代器的next()方法,循环便继续运行并停止在下一条yield语句处。

生成器的创建方式

生成器是个函数:

function *createIterator(items) { ... }

可以用函数表达式方式书写:

let createIterator = function *(item) { ... }

也可以添加到对象中,ES5风格对象字面量:

let o = {
    createIterator: function *(items) { ... }
};

let iterator = o.createIterator([1, 2, 3]);

ES6风格的对象方法简写方式:

let o = {
    *createIterator(items) { ... }
};

let iterator = o.createIterator([1, 2, 3]);
可迭代(Iterable)对象

在ES6中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,可迭代对象都绑定了默认的迭代器。

来了来了,姗姗来迟的ES6循环新特性for-of

var colors = ["red", "green", "blue"];
for(let color of colors){
    console.log(color);
}

for-of循环,可作用在可迭代对象上,正是利用了可迭代对象上的默认迭代器。大致过程是:for-of循环每执行一次都会调用可迭代对象的next()方法,并将迭代器返回的结果对象的value属性存储在变量中,循环将继续执行这一过程直到返回对象的done属性的值为true

如果只需要迭代数组或集合中的值,用for-of循环代替for循环是个不错的选择。

访问默认迭代器

可迭代对象,都有一个Symbol.iterator方法,for-of循环时,通过调用colors数组的Symbol.iterator方法来获取默认迭代器的,这一过程是在JavaScript引擎背后完成的。

我们可以主动获取一下这个默认迭代器来感受一下:

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获取了数组values的默认迭代器,并用它遍历数组中的元素。在JavaScript引擎中执行for-of循环语句也是类似的处理过程。

用Symbol.iterator属性来检测对象是否为可迭代对象:

function isIterator(object) {
    return typeof object[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3]));  // true
console.log(isIterable(new Set()));  // true
console.log(isIterable(new Map()));  // true
console.log(isIterable("Hello"));  // true
创建可迭代对象

当我们在创建对象时,给Symbol.iterator属性添加一个生成器,则可以将其变成可迭代对象:

let collection = {
    items: [],
    *[Symbol.iterator]() { // 将生成器赋值给对象的Symbol.iterator属性来创建默认的迭代器
        for(let item of this.items) {
            yield item;
        }
    }
};

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for(let x of collection) {
    console.log(x);
}
内建迭代器

ES6中的集合对象,数组、Set集合和Map集合,都内建了三种迭代器:

entries() 返回一个迭代器,其值为多个键值对。
如果是数组,第一个元素是索引位置;如果是Set集合,第一个元素与第二个元素一样,都是值。

values() 返回一个迭代器,其值为集合的值。

keys() 返回一个迭代器,其值为集合中的所有键名。
如果是数组,返回的是索引;如果是Set集合,返回的是值(Set的值被同时用作键和值)。

不同集合的默认迭代器

每个集合类型都有一个默认的迭代器,在for-of循环中,如果没有显式指定则使用默认的迭代器。按常规使用习惯,我们很容易猜到,数组和Set集合的默认迭代器是values(),Map集合的默认迭代器是entries()。

请看以下示例:

let colors = [ "red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");

// 与调用colors.values()方法相同
for(let value of colors) {
    console.log(value);
}

// 与调用tracking.values()方法相同
for(let num of tracking) {
    console.log(num);
}

// 与调用data.entries()方法相同
for(let entry of data) {
    console.log(entry);
}

这段代码会输入以下内容:

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]

for-of循环配合解构特性,操纵数据会更方便:

for(let [key, value] of data) {
    console.log(key + "=" + value);
}
用展开运算符操纵
let set = new Set([1, 2, 3, 4, 5]),
    array = [...set];
    
console.log(array);  // [1,2,3,4,5]

展开运算符可以操作所有的可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值。然后按返回顺序将它们依次插入到数组中。因此如果想将可迭代对象转换为数组,用展开运算符是最简单的方法。

迭代器高级功能 给迭代器传参

前面我们看到,在迭代器内部使用yield关键字可以生成值,在外面可以用迭代器的next()方法获得返回值。
其实next()方法还可以接收参数,这个参数的值就会代替生成器内部上一条yield语句的返回值。

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;  // 4 + 2
    yield second + 3;  // 5 + 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 }"

下图的阴影展示了每次yield前正在执行的代码,可以辅助理解程序内部的具体细节:


在生成器内部,浅红色高亮的是next()方法的第一次调用,浅绿色标识了next(4)的调用过程,紫色标示了next(5)的调用过程,分别返回每一次yield生成的值。这里有一个过程很复杂,在执行左侧代码前,右侧的每一个表达式会先执行再停止。
这里有个特例,第一次调用next()方法时无论传入什么参数都会被丢弃。由于传递给next()方法的参数会代替上一次yield的返回值,而在第一次调用next()方法前不会执行任何yield语句,因此在第一次调用next()方法时传递参数是毫无意义的。

在迭代器中抛出错误

除了给迭代器传递数据外,还可以给它传递错误条件。通过throw()方法,当迭代器恢复执行时可令其抛出一个错误。

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;  // yield 4 + 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")));  // 从生成器中抛出错误

这个示例中,前两个表达式正常求值,而调用throw()后,在继续执行let second求值前,错误就会被抛出并阻止代码继续执行。
知道了这一点,就可以在生成器内部通过try-catch代码块来捕获这些错误:

function *createIterator() {
    let first = yield 1;
    let second;
    
    try {
        second = yield first + 2;  // yield 4 + 2, 然后抛出错误
    } catch(e) {
        second = 6;
    }
    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")));  // "{ value: 9, done: false }"
console.log(iterator.next());                    // "{ value: undefined, done: true }"

这里有个有趣的现象:调用throw()方法后也会像调用next()方法一样返回一个结果对象。由于在生成器内部捕获了这个错误,因而会继续执行下一条yield语句,最终返回数值9。
如此一来,next()和throw()就像是迭代器的两条指令,调用next()方法命令迭代器继续执行(可能提供一个值),调用throw()方法也会命令迭代器继续执行,但同时抛出一个错误,在此之后的执行过程取决于生成器内部的代码。

生成器返回语句

由于生成器也是函数,因此可以通过return语句提前退出函数执行。

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

let iterator = createIterator();

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

这段代码中的生成器包含多条yield语句和一条return语句,其中return语句紧随第一条yield语句,其后的yield语句将不会被执行。
在return语句中也可以指定一个返回值:

function *createIterator() {
    yield 1;
    return 10;
}

let iterator = createIterator();

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

通过return语句指定的返回值,只会在返回对象中出现一次,在后续调用返回的对象中,value属性会被重置为undefined。

委托生成器

在某些情况下,需要将两个迭代器合二为一,这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他生成器。

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

function *createColorIterator() {
    yield "red";
    yield "green";
}

function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}

var 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: "green", done: false }"
console.log(iterator.next());      // "{ value: true, done: false }"
console.log(iterator.next());      // "{ value: undfined, done: true }"

有了委托生成器这个信功能,你可以进一步利用生成器的返回值来处理复杂任务:

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

function *createRepeatingIterator(count) {
    for(let i=0; i

注意,无论通过何种方式调用迭代器的next()方法,数值3永远不会被返回,它只存在于生成器createCombinedIterator()的内部。但如果想输出这个值,则可以额外添加一条yield语句:

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

function *createRepeatingIterator(count) {
    for(let i=0; i           
               
                                           
                       
                 

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

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

相关文章

  • ES6-迭代iterator)和生成generator) - 什么是迭代生成

    摘要:我个人认为迭代器和生成器是新增的特性里面,非常重要的部分,充分地掌握和使用迭代器和生成器,是十分必要和重要的,所以我会写关于二者的一系列文章。 我个人认为迭代器和生成器是es6新增的特性里面,非常重要的部分,充分地掌握和使用迭代器和生成器,是十分必要和重要的,所以我会写关于二者的一系列文章。话不多说,先来了解一下基本概念:一:什么是迭代器 1: 迭代器是一个对象 2: 迭代器有一个属性...

    CollinPeng 评论0 收藏0
  • ES6 的 for..of 和 Generator,从伪数组 jQuery 对象说起

    摘要:引用自可迭代对象和迭代器不以规矩,不成方圆为了使某个对象成为可迭代对象象,它必须实现方法,也就是说,它得有一个是的属性。的遍历,绝对应该用。 pseudo 英 [sju:dəʊ] 美 [su:doʊ]adj.假的,虚伪的n.[口]假冒的人,伪君子 pseudo-array 英 [sju:dəʊəreɪ] 美 [sju:dəʊəreɪ][计] 伪数组 jQuery 对象是伪数组 两个...

    Harriet666 评论0 收藏0
  • 【重温基础】13.迭代生成

    摘要:迭代器和生成器将迭代的概念直接带入核心语言,并提供一种机制来自定义循环的行为。本文主要会介绍中新增的迭代器和生成器。属性本身是函数,是当前数据结构默认的迭代器生成函数。 本文是 重温基础 系列文章的第十三篇。今日感受:每次自我年终总结,都会有各种情绪和收获。 系列目录: 【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理) 【重温基础】1.语法和数据类型 【重温基础】2.流...

    ymyang 评论0 收藏0
  • ES6-迭代iterator)和生成generator)- 迭代生成的高级用法

    摘要:在生成器中使用语句生成器也是函数,所以它也可以使用语句。只是由于生成器本身的特性,其内部的的行为会和一般函数有些差别。 前面2篇系列文章讲解了迭代器和生成器的最常用,最基础的用法;这篇来讨论迭代器和生成器的一些稍稍高级一点的用法: 1: 给迭代器的next()方法传参 2: 从迭代器中抛出错误 3: 在生成器中使用return语句 4: 委托生成器(组合生成器或者生成器组合?) 1: ...

    Edison 评论0 收藏0
  • ES6 Features系列GeneratorFunction介绍

    摘要:没有显示显示显示关键字迭代器生成器用于马上退出代码块并保留现场,当执行迭代器的函数时,则能从退出点恢复现场并继续执行下去。迭代器迭代器是一个拥有方法和方法的对象,通过函数不断执行以关键字分割的代码段,通过函数令分割的代码段抛出异常。 一、前言                            第一次看koajs的示例时,发现该语句 function *(next){..........

    golden_hamster 评论0 收藏0

发表评论

0条评论

DrizzleX

|高级讲师

TA的文章

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