八: ES6 迭代器 生成器

前言

该部分为书籍 深入理解ES6 第八章(迭代器与生成器)笔记

循环的问题

var colors = ["red", "green", "blue"];

for (var i = 0, len = colors.length; i < len; i++) {
    console.log(colors[i]);
}

虽然这个循环非常直观, 然而当它被嵌套使用并要追踪多个变量时, 情况就会变得非常复杂. 额外的复杂度会引发错误, 而 for 循环的样板性也增加了自身出错的可能性, 因为相似的代码会被写在多个地方

何为迭代器?

ES6 中的迭代器是被设计专用于迭代的对象, 带有特定接口. 其规则为: 所有的迭代器对象都拥有 next() 方法, 会返回一个结果对象, 这个结果对象有两个属性: 对应下一个值的 value, 以及一个布尔类型的 done, 其值为 true 时表示没有更多值可供使用.

迭代器持有一个指向集合位置的内部指针, 每当调用了 next() 方法, 迭代器就会返回相应的 下一个值

// 在 ES5 中创建一个 类迭代器
function createIterator(items) {
    
    var i = 0;
    
    return {
        next: function() {
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}

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: undefined, done: true }"

// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

何为生成器?

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

**生成器函数最有意思的方面可能就是它们会在每个 yield 语句后停止执行. ** yield 关键字可以和值或是表达式一起使用

// 在次函数内, for 循环在循环执行时从数组中返回元素给迭代器. 每当遇到 yield, 循环就会停止; 而每当 iterator 上的 next() 方法被调用, 循环就会再次执行到 yield 语句处.
function *createIterator(items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
}

let 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: undefined, done: true }"

// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

yield 关键字只能用在生成器内部, 用于其他任意位置都是语法错误 , 即使在生成器内部的函数也不行

function *createIterator(items) {
    items.forEach(function(item) {
        // 语法错误
        yield item + 1;
    });
}

生成器函数是 ES6 的一个重要特性, 并且因为它就是函数, 就能被用于所有可用函数的位置

1. 生成器函数表达式

**语法: *function 关键字与圆括号之间使用一个星号()即可.

注意: 箭头函数不能为生成器

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

let 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: undefined, done: true }"

// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

2. 生成器对象方法

生成器就是函数, 因此可以被添加到对象中

var o = {
    createIterator: function *(items) {
        for (let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

// ES6 的速记法
var o = {
    *createIterator(items) {
        for (let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

可迭代对象与for-of循环

与迭代器紧密相关的是, 可迭代对象(iterable)是包含 Symbol.iterator 属性的对象. 这个 Symbol.iterator 知名符号定义了为指定对象返回迭代器的函数.

在 ES6 中, 所有的集合对象(数组、Set与Map) 以及字符串都是可迭代对象, 因此它们都被指定了默认的迭代器, 可迭代对象被设计用于与 ES 新增的 for-of 循环配合使用

生成器创建的所有迭代器都是可迭代对象, 因为生成器默认就会为 Symbol.iterator 属性赋值

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

let values = [1, 2, 3];

// 这个 for-of 首先调用了 values 数组的 Symobol.iterator 方法, 获取了一个迭代器(发生在 JS 引擎后台). 接下来 iterator.next() 被调用,迭代器结果对象的 value 属性被读出并放入了 num 变量。 num 变量的值开始为 1 ,接下来是 2 ,最后变成 3 。当结果对象的 done 变成 true ,循环就退出了,因此 num 绝不会被赋值为 undefined 。

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

1. 访问默认迭代器

可以使用 Symbol.iterator 来访问可迭代对象上的默认迭代器

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 }"

// 或检测是否能进行迭代
function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}

2. 创建可迭代对象

自定义对象默认情况下不是可迭代对象, 但可以通过写入 Symbol.iterator 属性, 让它们成为可迭代对象

let collection = {
    items: [],
    *[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 的一个重要部分, 语言已经为许多内置类型创建了迭代器.

1. 集合的迭代器

ES6 具有三种集合对象类型: Set、数组和Map. 这三种都拥有如下的迭代器,有助于探索它们的内容:

  1. entries():返回一个包含键值对的迭代器

    entries() 迭代器会在每次 next() 被调用时返回一个双项数组, 对于数组来说, 第一项是数值索引; 对于 Set, 第一项也是值(因为它的值也会被视为键); 对于 Map, 第一项就是键

    let colors = [ "red", "green", "blue" ];
    let tracking = new Set([1234, 5678, 9012]);
    let data = new Map();
    
    data.set("title", "Understanding ES6");
    data.set("format", "ebook");
    
    for (let entry of colors.entries()) {
     console.log(entry);
    }
    
    for (let entry of tracking.entries()) {
     console.log(entry);
    }
    
    for (let entry of data.entries()) {
     console.log(entry);
    }
    
    // 此代码输出了如下内容:
    [0, "red"]
    [1, "green"]
    [2, "blue"]
    [1234, 1234]
    [5678, 5678]
    [9012, 9012]
    ["title", "Understanding ES6"]
    ["format", "ebook"]
    
  2. values()迭代器

    values() 迭代器仅仅能返回存储在集合内的值

    let colors = [ "red", "green", "blue" ];
    let tracking = new Set([1234, 5678, 9012]);
    let data = new Map();
    
    data.set("title", "Understanding ES6");
    data.set("format", "ebook");
    
    for (let value of colors.values()) {
     console.log(value);
    }
    
    for (let value of tracking.values()) {
     console.log(value);
    }
    
    for (let value of data.values()) {
     console.log(value);
    }
    
    // 此代码输出了如下内容:
    "red"
    "green"
    "blue"
    1234
    5678
    9012
    "Understanding ES6"
    "ebook"
    
  3. keys()迭代器

    keys() 迭代器 能返回集合中的每一个键. 对于数组, 返回数值类型的 键, 永不返回数组的其他自由属性; Set 的键与值是相同的, 因此它的 keys()values() 返回了相同的 迭代器; 对于 Map, keys() 迭代器返回了每个不重复的键

    let colors = [ "red", "green", "blue" ];
    let tracking = new Set([1234, 5678, 9012]);
    let data = new Map();
    
    data.set("title", "Understanding ES6");
    data.set("format", "ebook");
    
    for (let key of colors.keys()) {
     console.log(key);
    }
    for (let key of tracking.keys()) {
     console.log(key);
    }
    for (let key of data.keys()) {
     console.log(key);
    }
    
    // 本例输出了如下内容:
    0
    1
    2
    1234
    5678
    9012
    "title"
    "format"
    
  4. 集合类型的默认迭代器

    for-of 循环没有显式指定迭代器时, 每种集合类型都有一个默认的迭代器供循环使用

    values() 方法是数组与 Set 的默认迭代器, 而 entries() 方法则是 Map 的默认迭代器.

2. 字符串的迭代器

从 ES5 发布开始, JS 的字符串就慢慢变得越来越像 数组. 而字符串也是由默认迭代器的

var message = "A B" ;
for (let c of message) {
    console.log(c);
}

// 此代码输出了如下内容:
A
(blank)

(blank)
B

3. NodeList 的迭代器

文档对象模型(DOM)具有一种 NodeList 类型, 用于表示页面文档中元素的集合.

NodeList也包含了一个默认迭代器, 其表现方式与数组的默认迭代器一致.

var divs = document.getElementsByTagName("div");

for (let div of divs) {
    console.log(div.id);
}

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

扩展运算符(...)可以被用于将一个 Set 转换为数组

扩展运算符能作用于所有可迭代对象, 并且会使用默认迭代器来判断需要使用哪些值. 所有的值都从迭代器中被读取出来并插入数组, 遵循迭代器返回值的顺序

let map = new Map([ ["name", "Nicholas"], ["age", 25]]),
    array = [...map];

console.log(array); // [ ["name", "Nicholas"], ["age", 25]]

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
    array = [...set];

console.log(array); // [1,2,3,4,5]

迭代器的高级功能

使用迭代器的基本功能, 并使用生成器来方便地创建迭代器, 可以完成很多工作了, 然而, 在单纯迭代集合的值之外的任务中, 迭代器会显得更加强大

1. 传递参数给迭代器

前面的例子中已经展示了迭代器能够将值传递出来, 通过 next() 方法或者在生成器中使用 yield 都可以. 还能通过 next() 方法向迭代器传递参数 ==> 数据的双向流通

function *createIterator() {
    // yield 1 => 会将语句的结果值(1)传递出去给第一个 next() 方法的结果对象
    // (yield 1) => 表达式会接收到第二个 next(4) 传递进来的参数(4) 并赋值给 first 变量
    let first = yield 1;
    
    // yield first + 2 => 会将语句的结果值(6)传递出去给第二个 next(4) 方法的结果对象
    // (yield first + 2) => 表达式会接收到第三个 next(5) 传递进来的参数(5) 并赋值给 second 变量
    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 }"

执行机制图: 黄色表示对于 next() 的第一次调用、以及在生成器内部执行的所有代码;水蓝色表示了对next(4) 的调用以及随之执行的代码;而紫色则表示对 next(5) 的调用以及随之执行的代码

传递参数给迭代器.png

2. 在迭代器中抛出错误

能传递给迭代器的不仅是数据, 还可以是错误条件, 迭代器可以通过 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() 被调用时所执行的代码,红星说明了错误在生成器内部大约何时被抛出。

在迭代器中抛出错误.png

可以在生成器内部使用一个 try-catch 块来捕捉错误

function *createIterator() {
    let first = yield 1;
    let second;
    
    try {
        second = yield first + 2; // yield 4 + 2 ,然后抛出错误
    } catch (ex) {
        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 }"

// throw() 方法就像 next() 方法一样返回了一个结果对象. 由于错误在生成器内部被捕捉, 代码继续执行到下一个 yield 处并返回了下一个值, 也就是 9
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"

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

next() 方法指示迭代器继续执行(可能会带着给定的值), 而 throw() 方法则指示迭代器通过抛出一个错误继续执行. 在调用点之后会发生什么, 根据生成器内部的代码来决定

next()throw() 方法控制着迭代器在使用 yield 时内部的执行

3. 生成器的 Return 语句

生成器也是函数, 同样在它内部使用 return 语句, 这样会使生成器退出执行, 同样也可以指定在 next() 方法最后一次调用时的返回值

在生成器内, return 表明所有的处理已完成, 因此 done 属性会被设为 true, 而如果提供了返回值, 就会被用于 value 字段

function *createIterator() {
    yield 1;
    return 42;
    yield 2;
}
let iterator = createIterator();

console.log(iterator.next()); // "{ value: 1, done: false }"
// 第二次调用时, return 语句后的 42 会被返回在 value 字段中
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

扩展运算符与 for-of 循环会忽略 return 语句所指定的任意值. 一旦它们看到 done 的值为 true, 它们就会停止操作而不会读取对应的 value

4. 生成器委托

生成器可以用星号()配合 yield 这一特殊形式来委托其他迭代器*

function *createNumberIterator() {
    yield 1;
    yield 2;
}
function *createColorIterator() {
    yield "red";
    yield "green";
}
// createCombinedIterator() 生成器依次委托了 createNumberIterator() 与 createColorIterator().
function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}

var iterator = createCombinedIterator();

// 每次对 next() 的调用都会委托给合适的生成器, 直到使用 createNumberIterator() 与 createColorIterator() 创建迭代器全部清空为止
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: undefined, done: true }"

生成器委托也能让你进一步使用生成器的返回值, 这是访问这些返回值的最简单方式

function *createNumberIterator() {
    yield 1;
    yield 2;
    // 这里只有 return 返回值才会被 result 接收, 使用 yield 是无效的
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

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

可以直接在字符串上使用 yield *(例如 `yield * "hello"), 字符串的默认迭代器会被使用

异步任务运行

以后在进一步了解

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

推荐阅读更多精彩内容