《Understanding ES6》chapter 8 Iterators and Generators

264 阅读11分钟

2019-7-24

第八章 迭代器与生成器

for循环的问题: for循环嵌套使用容易写错循环变量,复杂度变高。

(一) 何为迭代器

迭代器:

  1. 有一个指向集合位置的内部指针;
  2. 有next()方法,调用时返回一个包含value属性和done属性的对象;
  3. 每调用一次next方法,迭代器就会返回相应的下一个值;
  4. 若在最后一个值被返回后再调用next,所返回的done属性为true,且value属性值为迭代器自身的返回值,即为用return语句明确返回的值,未提供时会返回undefined。

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

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

(二)何为生成器

1. 生成器的含义

ES5创建迭代器的方式比较复杂,ES6提供了生成器来创建迭代器。生成器:

  1. 是可以返回一个迭代器的函数,能被用于所有可使用函数的位置;
  2. 由放在function后面的*来表示, ES6没有规定function与*之间是否要有空格;
  3. 能使用yield关键字,yield指定了next()方法调用迭代器时应当按顺序返回的值,生成器会在每个yield语句后停止执行。

ES6用生成器来创建迭代器

// generator能像普通函数一样被调用,但会返回一个迭代器
function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}

// generator能像普通函数一样被调用,但会返回一个迭代器
let iterator = createIterator();

console.log(iterator.next().value);     // 1
console.log(iterator.next().value);     // 2
console.log(iterator.next().value);     // 3

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

function *createIterator(items) {

    items.forEach(function(item) {

        // syntax error
        yield item + 1;
    });
}

2. 生成器写法

1)函数声明 function* name() {}

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

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

2)函数表达式 function* () {}

// 函数表达式
let createIterator = function* (items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
};

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

不能用箭头函数创建生成器

3)对象方法

createIterator: function *(){}

*createIterator() {}

var o = {
		// ES5 对象字面量的写法
    createIterator: function* (items) {
        for (let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

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

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

这三种写法生成器的功能都没有区别,只是有语法区别。

(三)可迭代对象

1. 可迭代对象

可迭代对象(iterable)是包含Symbol.iterator属性的对象,与迭代器紧密相关。

Symbol.iterator定义了为指定对象返回迭代器的函数。

ES6中所有的集合对象(数组、Set、Map)和字符串都是可迭代对象,有默认的迭代器,注意自定义创建的对象不是可迭代的。

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

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

for-of循环在循环每次执行时都会调用可迭代对象的next()方法,并将结果对象的value值存储在一个变量上,循环过程会持续到结果对象的done属性变成true为止。

let values = [1, 2, 3];

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

for-of循环的执行过程:

  • 调用values数组的Symbol.iterator方法,创建一个迭代器;(此调用由js引擎在后台执行)
  • 调用迭代器的next()方法,读取结果对象的value属性并放入到num变量;
  • 当结果对象的done变成true,循环立即结束,不会再去读取value属性,所以num变量的值不会为undefined。

若仅需要迭代数组和集合的值,推荐使用for-of循环而非for循环。

在不可迭代对象、null或undefined上使用for-of语句,会抛出错误。

3. Symbol.iterator

1)使用Symbol.iterator访问对象上的默认迭代器

// 此代码获取了values数组的默认迭代值,并用它来迭代数组中的项。这与使用for-of循环时在后台发生的过程一致。
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 }"

2)使用Symbol.iterator检测一个对象是否能被迭代

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

console.log(isIterable([1, 2, 3]));     // true
console.log(isIterable("Hello"));       // true
console.log(isIterable(new Map()));     // true
console.log(isIterable(new Set()));     // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

3)使用Symbol.iterator自定义可迭代对象 自定义对象默认是不可迭代的,可以创建一个包含生成器的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);
}

4. 可迭代对象与扩展运算符

扩展运算符(…)可作用于所有可迭代对象,且会使用默认迭代器来判断需要使用哪些值。

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

console.log(array);             // [1,2,3,4,5]
let map = new Map([ ["name", "Nicholas"], ["age", 25]]),
    array = [...map];

console.log(array);         // [ ["name", "Nicholas"], ["age", 25]]
let smallNumbers = [1, 2, 3],
    bigNumbers = [100, 101, 102],
    allNumbers = [0, ...smallNumbers, ...bigNumbers];

console.log(allNumbers.length);     // 7
console.log(allNumbers);    // [0, 1, 2, 3, 100, 101, 102]

(四)内置的迭代器

1. 集合的迭代器

ES6有三种集合对象类型: 数组、Map和Set,他们都拥有以下三种迭代器:

  • entries(): 返回键值对
  • values(): 返回值
  • keys(): 返回键

集合类型的默认迭代器:

数组和Set类型的默认迭代器是:values();

Map的默认迭代器是:entries()

Map默认迭代器的行为有助于在for-of循环中使用解构:

let data = new Map();

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

// same as using data.entries()
for (let [key, value] of data) {
    console.log(key + "=" + value);
}

2. 字符串的迭代器

字符串的默认迭代器能处理字符,而不是码元。

var message = "A ð ®· B";

for (let c of message) {
    console.log(c);
}
A

ð

®
·

B

3. NodeList的迭代器

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

NodeList默认迭代器的规定在HTML规范而非ES6规范中,其表现方式与数组的默认迭代器一致,因此可以将NodeList用于for-of循环,或其他使用对象默认迭代器的场合。

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

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

(五) 迭代器高级功能

1. 传参给迭代器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 }"

Figure 8-1: Code execution inside a generator

next()方法的运行逻辑:

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的结果对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

注意:由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数时无效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带参数。

2. 在迭代器中抛出错误throw()

能传递给迭代器的不仅是数据,还可以是错误条件。使用throw()方法,让迭代器在恢复执行时可以抛出一个错误。

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // yield 4 + 2, then throw
    yield second + 3;                   // never is executed
}

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"))); // error thrown from generator

Figure 8-2: Throwing an error inside a generator

可以在生成器内部使用try-catch捕捉错误:

function *createIterator() {
    let first = yield 1;
    let second;

    try {
        second = yield first + 2;       // yield 4 + 2, then throw
    } catch (ex) {
        second = 6;                     // on error, assign a different value
    }
    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 }"

将next()和throw()都当做迭代器的指令,会有助于思考。next()方法只是迭代器继续执行(可能会带着给定的值),而throw()方法则指示迭代器通过抛出一个错误继续执行。在调用点之后会发生什么,根据生成器内部的代码来决定。

3. 生成器的return语句

生成器是函数,可以使用return语句,让生成器提早退出执行,也可以用return指定在next()方法最后一次调用时的返回值。

function *createIterator() {
    yield 1;
    return;    // 让生成器提早退出执行 后面的yield都是不可达的
    yield 2;
    yield 3;
}

let iterator = createIterator();

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

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

let iterator = createIterator();

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

注意:

  • 在return语句中指定的任意值都只会在结果对象中出现一次,此后value字段就会被重置为undefined。
  • 扩展运算符(…)和for-of循环会忽略return语句所指定的值,一旦他们看懂啊done的值为true,就会停止操作而不会读取对应的value值。

4. 生成器委托

生成器可以用*配合yield来委托其他的迭代器。

      // 生成器委托
      function* createNumberIterator() {
        yield 1
        yield 2
      }

      function* createColorIterator() {
        yield 'red'
        yield 'green'
      }

      function* createCombinedIterator() {
        // 生成器依次委托了createNumberIterator()和createColorIterator()
        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: undefined, done: true }"

使用生成器的返回值:

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

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

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    // yield result;
    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: 3, 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 }"

注意,上面例子中的值3从未在对于next()方法的任何调用中被输出,当前它仅仅存在于createCombinedIterator()生成器内部,除非我们把yield result;的注释去掉,用yield语句明确将此返回值输出。

(六)异步任务运行

最常见的异步调用方式:使用回调函数,容易形成回调地狱。

而yield能停止运行,并在重新开始运行前等待next()方法被调用,可以在没有回调函数的情况下实现异步调用。

1. 一个简单的任务运行器

// taskDef是一个生成器函数
function run(taskDef) {

    // 创建迭代器,让它在别处可用
    let task = taskDef();

    // start the task
    let result = task.next();

    // 递归调用函数来保持对next()的调用
    function step() {

        // if there's more to do
        if (!result.done) {
            result = task.next();
            step();
        }
    }

    // start the process
    step();

}

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

2. 带数据的任务运行器

更改上面例子中step()函数的方法体内容:

    function step() {
      
        if (!result.done) {
          	// 将result.value作为next()的参数
            result = task.next(result.value);
            step();
        }
    }
    
    run(function*() {
    let value = yield 1;
    console.log(value);         // 1

    value = yield value + 3;
    console.log(value);         // 4
});

3. 异步任务运行器

传统的执行异步操作的方式是调用一个包含回调的函数,例如:考虑在Node.js中从磁盘读取一个文件:

let fs = require("fs");

fs.readFile("config.json", function(err, contents) {
    if (err) {
        throw err;
    }

    doSomethingWith(contents);
    console.log("Done");
});

当需要完成的任务很少时,这么做很有效,然而当需要嵌套回调函数,或者要按顺序处理一系列的异步任务时,此方式就会非常麻烦,在这种场合下,生成器和yield会带来帮助:

function run(taskDef) {

    // create the iterator, make available elsewhere
    let task = taskDef();

    // start the task
    let result = task.next();

    // recursive function to keep calling next()
    function step() {

        // if there's more to do
        if (!result.done) {
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }

                    result = task.next(data);
                    step();
                });
            } else {
                result = task.next(result.value);
                step();
            }

        }
    }

    // start the process
    step();

}

let fs = require("fs");

function readFile(filename) {
    return function(callback) {
        fs.readFile(filename, callback);
    };
}

run(function*() {
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});

此例执行了异步的readFile()操作,而在主要代码中并未暴露出任何回调函数。除了yield之外,此代码看起来与同步代码并无不同。