ES6学习笔记:迭代器与生成器

许多编程语言都将迭代数据的方式从使用 for 循环转变到使用迭代器对象。

  • for循环需要初始化变量以便追踪集合内的位置
  • 迭代器则以编程方式返回集合中的下一项,能使操作集合变得简单。

普通的迭代方式:

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

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

for循环使用变量i来追踪 colors 数组中的位置索引。

虽然这个循环非常直观,然而当它被嵌套使用并要追踪多个变量时,情况就会变得非常复杂。迭代器正是用来解决此问题的。

什么是迭代器

迭代器是专用于迭代的一个对象,带有特定接口:

  • 所有迭代器对象都拥有next()方法,会返回一个结果对象。此对象拥有两个属性:

    • value:下一个值
    • done:true表示迭代结束,没有更多值了
  • 迭代器持有一个指向集合位置的内部指针,每当调用了next方法,迭代器就会返回下一个值。

  • 若在最后一个值返回后再调用next(),所返回的对象:
    • done:true
    • value:迭代器自身的返回值(即使用return语句明确返回的值)
      迭代器未提供返回值则为undefined。
      迭代器自身的返回值类似于函数的返回值,是向调用者返回信息的最后手段。

创建一个迭代器:

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]);

iterator.next(); //{done:false, value:1}
iterator.next(); //{done:false, value:2}
iterator.next(); //{done:false, value:3}
iterator.next(); //{done:true, value:undefined}

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

createIterator() 函数返回一个带有 next() 方法的对象,通过一次次地调用next对象实现了迭代,并在迭代结束后再次调用next只会返回undefined。

但是自己写生成迭代器的函数很麻烦,还好有生成器,让创建迭代器对象变得更简单。

什么是生成器

生成器generator是一个能返回迭代器的函数。
生成器函数由function关键字后的一个*来表示,并可以使用yield关键字。

//生成器
function *createIterator(){
    yield 1;
    yield 2;
    yield 3;
}

//调用生成器会返回一个迭代器
let iterator = createIterator();

iterator.next().value;  //1
iterator.next().value;  //2
iterator.next().value;  //2
  • 型号*紧跟在function后面,或者中间留出空隙都是可以的。
  • yield关键字指定了迭代器在被next()方法调用时应当按顺序返回的值。
  • 生成器能像其他函数一样被调用。

生成器函数最特别的地方就是它会在每个yield语句后停止执行。
例如,此代码中 yield 1 执行后,该函数将不会再执行任何操作,直到迭代器的 next() 方法被调用,此时才继续执行 yield 2 。

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

生成器接受了一个数组作为参数。for循环中每次遇到yield,循环就会停止;当Iterator的next方法被调用时,循环就会再次执行到yield处。

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

function *createIterator(items) {

  items.forEach(function(item) {

      // 语法错误
      yield item + 1;
  });
}

这里yield被用在了forEach的回调函数内部,因此出错。

生成器函数表达式

也可以使用函数表达式来创建一个生成器,只要在function 关键字与圆括号之间使用一个星号( * )即可:

let createIterator = function *(items){
    ……
}

此代码中的 createIterator() 是一个生成器函数表达式,而不是一个函数声明。星号放置在 function 关键字与圆括号之间,是因为这个函数表达式是匿名的。

不能将箭头函数创建为生成器。

生成器对象方法

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

var o = {

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

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

也可以使用 ES6 方法的速记法:

var o = {

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

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

由于此时没有function关键字,因此就要将星号*放在方法名之前。

可迭代对象与 for-of 循环

可迭代对象是包含 Symbol.iterator 属性的对象。这个 Symbol.iterator属性定义了为指定对象返回迭代器的函数

ES6中的可迭代对象:

  • 集合对象:
    • 数组
    • Set
    • Map
  • 字符串

它们都被指定了默认的迭代器。

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

可迭代对象被设计用于与 ES 新增的 for-of 循环配合使用。for-of 循环与for循环相比,完全删除了追踪集合索引的需要,让你无拘束地专注于操作集合内容。

for-of循环的工作过程:

  • 在每次循环执行时会调用可迭代对象的 next() 方法,并将结果对象的 value 值存储在一个变量上
  • 循环过程会持续到结果对象的 done 属性变成 true 为止
let values = [1, 2, 3];

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

//1
//2
//3
  • for-of 循环首先调用了 values 数组的 Symbol.iterator 方法,获取了一个迭代器
  • iterator.next() 被调用,迭代器结果对象的 value 属性被读出并放入了 num 变量
  • 当结果对象的 done 变成 true ,循环就退出了,因此 num 绝不会被赋值为 undefined 。

如果你只是简单地迭代数组或集合的值,那么使用 for-of 循环而不是 for 循环就是个好主意。 for-of 循环一般不易出错,因为需要留意的条件更少;传统的 for 循环被保留用于处理更复杂的控制条件。

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

访问默认迭代器

你可以使用 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 }"

Symbol.iterator属性的值是一个生成迭代器的函数,我们调用这个函数,得到iterator,并用它来迭代数组中的项。

既然 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

这个 isIterable() 函数仅仅查看对象是否存在一个类型为函数的默认迭代器。 for-of 循环在执行之前会做类似的检查。

创建可迭代对象

你可以为为一个对象添加值为生成器的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);
}

//1
//2
//3
  • 首先为collection对象定义了一个默认的迭代器,这个默认迭代器是用 Symbol.iterator 方法创建的,此方法是一个生成器(注意名称之前依然有星号)。
  • 接下来该生成器使用了一个 for-of 循环来对 this.items 中的值进行迭代,并使用了 yield 来返回每个值。
  • collection 对象依靠 this.items 的默认迭代器来工作,而非在定义的值上手动进行迭代。

内置的迭代器

许多内置类型默认包含迭代器,只有当内置的迭代器无法满足你的需要时,才有必要创建自定义迭代器。

集合的迭代器

最常用的迭代器就是集合上的迭代器。
ES6具有三种集合对象类型:数组、Map、Set。这三种类型都拥有如下的迭代器:

  • entries():返回一个包含键值对的迭代器
  • values():返回一个包含集合中的的迭代器
  • keys():返回一个包含集合中的的迭代器

你可以调用上述方法之一来提取集合中的迭代器。

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);
}
//[0, "red"]
//[1, "green"]
//[2, "blue"]


//Set
for(let entry of tracking.entries()){
     console.log(entry);
}
//[1234, 1234]
//[5678, 5678]
//[9012, 9012]

//Map
for (let entry of data.entries()) {
    console.log(entry);
}
//["title", "Understanding ES6"]
//["format", "ebook"]

此代码在每种集合类型上使用了 entries() 方法来提取迭代器,并且使用 for-of 循环来迭代它们的项。

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);
}
//"red"
//"green"
//"blue"

for (let value of tracking.values()) {
    console.log(value);
}
//1234
//5678
//9012


for (let value of data.values()) {
    console.log(value);
}
//"Understanding ES6"
//"ebook"

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);
}
//0
//1
//2

for (let key of tracking.keys()) {
    console.log(key);
}
//1234
//5678
//9012


for (let key of data.keys()) {
    console.log(key);
}
//"title"
//"format"

对于数组对象来说,只有数值类型索引被打印了,即使你向数组添加了具名属性也依然如此。这与在数组上使用 for-in 循环是不同的,因为 for-in 循环会迭代所有属性而不仅是数值索引

集合类型的默认迭代器

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

  • 数组与Set:values()是默认迭代器
  • Map:entries()是默认迭代器

使用for-of循环迭代集合对象时,就会使用这些默认的迭代器:

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

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

// 与使用 colors.values() 相同
for(let value of colors){
     console.log(value);
}
//"red"
//"green"
//"blue"

// 与使用 tracking.values() 相同
for (let num of tracking) {
    console.log(num);
}
//1234
//5678
//9012

// 与使用 data.entries() 相同
for (let entry of data) {
    console.log(entry);
}
//["title", "Understanding ES6"]
//["format", "print"]

数组与Set默认输出了它们的值,而Map返回的则是可以直接传给Map构造器的数组格式。
Weak Set 与 Weak Map 并未拥有内置的迭代器,使用弱引用意味着没有方法可以迭代这些值。

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

let data = new Map();

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

for(let [key,value] of data){
    console.log(key);
    console.log(value);
}

此代码中的 for-of 循环使用了数组解构,来将 Map 中的每个项存入 key 与 value 变量。使用这种方式,你能轻易同时处理键与值,而无须访问一个双项数组.

字符串的迭代器

for-of可用于字符串,这样就会以字符为单位输出:

var message = "A B";

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

NodeList的迭代器

DOM中的No的List类型,用于表示元素的集合。但NodeList只是一个类数组对象,拥有length属性,但与数组的行为是完全不同的。

但NodeList也包含了一个默认迭代器,其表现方式与数组的默认迭代器一致。这意味着你可以将 NodeList 用于 for-of 循环:

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

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

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

扩展运算符…可以将一个Set转换为数组:

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

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

此代码使用扩展运算符将set中的值填充到数组。

扩展运算符能作用于所有可迭代对象,并使用默认迭代器来判断需要使用哪些值
在这里,所有的值都从迭代器中被读取中来并插入到数组中。

这种方式可以应用于任何可迭代对象:

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

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

此处的扩展运算符将map转换为一个由数组构成的数组。由于Map的默认迭代器返回的是键值对,因此最终的结果也是一个由双项数组构成的数组。

你可以不限次的在数组字面量中使用扩展运算符,并且可以在任意位置使用扩展运算符将可迭代对象的多个项插入到数组中:

let smallNumbers = [1, 2, 3],
    bigNumbers = [100, 101, 102];
let allNumbers = [0, ...smallNumbers, ...bigNumbers];

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

此处的扩展运算符使用 smallNumbers 与 bigNumbers 中的数据来创建 allNumbers 数组。在 allNumbers 被创建时,值在其中的排列顺序与数组被添加的顺序一致: 首先是 0 ,其次是来自 smallNumbers 数组的元素,最后是来自 bigNumbers 数组的元素。
原始数组并没有被改变,只是它们的值被复制到了 allNumbers 数组中。

总结一下就是,扩展运算符应用在可迭代对象上,可以将可迭代对象转换为数组。

迭代器高级功能

给迭代器传递参数

迭代器能够将值传递出来,使用next()或者生成器中的yield都可以。
但是你也可以通过next()方法向迭代器传递参数。当一个参数被传递给next()方法时,该参数就会成为生成器内部yield语句的值。

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

let iterator = createIterator();

iterator.next(); //{value:1,done:false};
iterator.next(4); //{value:6,done:false};
iterator.next(5); //{value:8,done:false};
iterator.next(); //{value:undefined,done:true};
  • 第一次调用next()是一个特殊情况,传给next()的任意参数都会被忽略
    由于传递给 next() 的参数会成为 yield 语句的值,该 yield 语句指的是上次生成器中断执行处的语句;而 next() 方法第一次被调用时,生成器函数才刚刚开始执行,没有上一个 yield 语句。因此在第一次调用 next() 时,不存在任何向其传递参数的理由。

  • 第二次调用next()时,4作为参数被传递进去,最终被赋给了first变量。
    let first = yield 1;这个语句,在第一次next()调用时,右侧的 yield 1被计算;而表达式左侧的first则在第二次调用next()方法时、并在生成器函数继续执行前被计算,此时传进来的参数为yield 1语句的值,也就是first变量的值。然后生成器继续执行。
    第二个yield使用了first,因此返回的值是4+2为6.

  • 第三次调用next()时,与第二次相同,传入的参数5将作为上次执行的yield first+2语句的值,也就是second变量的值,因此返回了5+3 = 8

这个过程可以用下面的图片表示:

这里写图片描述

在迭代器中抛出错误

迭代器可以选择实现一个throw()方法,用于指示迭代器应在恢复执行时抛出一个错误。
这是对异步编程来说很重要的一个能力,同时也会增加生成器内部的灵活度,能够既模仿返回一个值,又模仿抛出错误(也就是退出函数的两种方式)。

你可以传递一个错误对象给 throw() 方法,当迭代器继续进行处理时应当抛出此错误:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 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被调用时,一个错误在second被运算之前就被抛出了,此时代码停止执行。因此最后的yield second + 3永远不会被执行。

这里写图片描述

可以看出错误是什么时候被抛出的:在throw的时候,在任何其他代码执行之前错误就被抛出了。

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

function *createIterator(){
    let first = yield 1;
    let second;
    try{
        second = yield first+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调用时,yield first+2语句对变量second赋值前就抛出了错误,被try-catch捕捉到,此时second被赋值为6,然后再执行到下一个yield处并返回了9.

此时,throw() 方法就像 next() 方法一样返回了一个结果对象。由于错误在生成器内部被捕捉,代码继续执行到下一个 yield 处并返回了下一个值,也就是 9 。

比较一下next方法与throw方法:

  • next方法指示迭代器继续执行(可能会带着给定的值)
  • throw则指示迭代器通过抛出一个错误来继续执行

在调用点之后发生什么完全由生成器内部代码决定。

生成器的return语句

生成器也是普通函数,你可以在它内部使用return语句,作用有下:

  • 可以使生成器早一点退出执行
  • 也可以指定在next()方法最后一次调用时的返回值。

在之前的例子中,对next()的最后一次调用value值都返回了undefined,但你也可以像其他函数一样,使用return来指定另一个返回值。

在生成器内,return表明所有处理已完成,因此done属性会设为true,返回值则被赋予给value字段。

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

第二次调用next()时,遇到了return语句,则此生成器提前返回,剩余的yield语句就不会再执行。

也可以指定一个返回值:

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

当第二次调用 next() 方法时,值 42 被返回在 value 字段中,此时 done 字段的值才第一次变为了 true。
第三次调用 next() 返回了一个对象,其 value 属性再次变回 undefined。
在return语句中指定的值只会被返回一次,此后value字段还是会被置为undefined。

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

生成器委托

在一个生成器内部可以使用yield语句配合*来委托其他迭代器。

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

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

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

var iterator = createCombinedIterator();

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

此例中的 createCombinedIterator() 生成器依次委托了 createNumberIterator() 与 createColorIterator() 。
createCombinedIterator()返回的迭代器看起来就是一个单一的迭代器,每次对next()的调用都会委托给合适的迭代器。直到使用 createNumberIterator() 与 createColorIterator() 创建的迭代器全部清空为止。然后最终的 yield 会被执行以返回 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 *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

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

此处 createCombinedIterator() 生成器委托了 createNumberIterator() 并将它的返回值3赋值给了 result 变量。result又被传进了createRepeatingIterator() 生成器,指示输出3次“repeat”。

当createNumberIterator返回时,下一个执行的yield语句是createRepeatingIterator中的yield "repeat";。这中间并没有输出result,除非你自己添加一个yield语句来输出这个值:

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

异步任务

由于yield能停止运行,并在重新开始运行前等带next()方法被调用,因此就可以实现异步调用。
首先需要一个能够调用生成器并启动迭代器的函数:

function run(taskDef){
    //调用生成器创建迭代器。
    let task = taskDef();

    //启动迭代器
    let result = task.next();

    //递归使用函数来保持对 next() 的调用
    function step(){
        if(!result.done){
            result = task.next();
            step();
        }
    }
    step();
}

step() 函数查看 result.done 是否为 false ,如果是就在递归调用自身之前调用 next() 方法。每次调用 next() 都会把返回的结果保存在 result 变量上,它总是会被最新的信息所重写。

配合这个已实现的 run() 函数,你就可以运行一个包含多条 yield 语句的生成器:

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

此例只是将三个数值输出到控制台,单纯用于表明对 next() 的所有调用都已被执行。

带数据的任务运行

传递数据给迭代器最简单的方式,就是把 yield 返回的值传入下一次的 next() 调用:

function run(taskDef){
    let task = taskDef();

    let result = task.next();

    function step(){
        if(!result.done){
            result = task.next(result.value);
            step();
        }
    }

    step();
}

现在 result.value 作为参数被传递给了 next() ,这样就能在 yield 调用之间传递数据了

run(function*() {
    let value = yield 1;
    console.log(value);         

    value = yield value + 3;
    console.log(value);         
});
  • 第一次调用tast.next(),执行到yield 1,此时result为{value:1,done:false}
  • 第二次调用task.next(),将result的value值1传入,此时在生成器内部,let value = yield 1语句左侧的value被赋值为1,控制台输出1。接下来指定到yield value + 3也就是4,此时result的value值为4
  • 第三次调用task.next(),将result的value值4传入,value = yield value + 3;语句使得左侧的value又被赋值为4,此时控制台输出4. result为{value:undefined,done:true},迭代结束

现在数据在对 yield 的调用之间流动了起来,仅需再来个微小改动,就能支持异步调用。

异步任务运行器

上个例子只是在 yield 之间来回传递静态数据,但等待一个异步处理与此稍微有点差异。迭代器需要了解回调函数,并了解如何使用它们。并且由于 yield 表达式将它们的值传递给了迭代器,这就意味着任意函数调用都必须返回一个值,并以某种方式标明该返回值是个异步操作调用,而迭代器应当等待此操作。

此处是将返回值标明为异步操作的一种方法:

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

此例的目的是:任何打算让任务运行器调用的函数,都应当返回一个能够执行回调函数的函数。‘’
fetchData函数返回的函数接收一个回调函数作为参数,当返回的函数被调用时,回调函数会被执行并传递一些数据。此回调函数需要由任务运行器提供。
我们现在将fetchData()函数改为异步的:

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

在深入理解函数如何标注自己是一个异步处理后,你就可以结合这种模式来改造任务运行器。只要 result.value 是一个函数,任务运行器就应当执行它,而不是仅仅将它传递给 next() 方法:

funcion run(taskDef){
    let task = taskDef();
    let result = task.next();

    function step(){
        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();
            }
        }
    }
    step();
}

当 result.value 是个函数时,它会被调用,并传入一个回调函数作为参数。
此时利用run函数来进行一个异步任务:

let fs = require("fs");

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

这个 readFile() 方法接受单个参数,即文件名,并返回一个能执行回调函数的函数。此回调函数会被直接传递给 fs.readFile() 方法,后者会在操作完成后执行回调。

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

此例执行了异步的 readFile() 操作:

  • 在任务运行器内部第一次执行task.next()时,readFile的执行结果被返回给result,即那个内部函数。当我们监测到result的值是一个函数时就执行它,并传入回调函数。
  • fs.readFile异步任务开始执行,当执行完毕后就会调用我们传入的callback函数。

    • 若 err 非空,也就表示有错误发生,需要使用该错误对象去调用 task.throw()
    • 若不存在错误, data 参数将会被传入 task.next() ,而其调用结果也会被保存在contents中。接下来,调用 step() 来继续处理过程。
  • 若 result.value 并非函数,它就会被直接传递给 next() 方法。

此例执行了异步的 readFile() 操作,而在主要代码中并未暴露出任何回调函数。除了 yield 之外,此代码看起来与同步代码并无二致。既然执行异步操作的函数都遵循了同一接口,你就可以用貌似同步的代码来书写处理逻辑。

总结

  • 可迭代对象:有Symbol.iterator属性的对象。
    • Symbol.iterator:定义对象的迭代器。内置对象与开发者自定义对象都可以使用这个符号,以提供一个能返回迭代器的方法。
  • for-of循环用于迭代可迭代对象。
    与使用传统 for 循环进行迭代相比,使用 for-of 要容易得多,因为你不再需要追踪计数器并控制循环何时结束。 for-of 循环会自动从迭代器中读取所有数据,直到没有更多数据为止,然后退出循环。

  • ES6中的许多类型都有默认的迭代器,在使用for-of的时候可以更方便。(集合对象:数组、Map、Set;字符串)

  • 扩展运算符能操作任意的可迭代对象,同时也能更简单地将可迭代对象转换为数组。转换工作会从一个迭代器中读取数据,并将它们依次插入数组。

  • 生成器是一个特殊的函数,在调用时会创建一个迭代器。生成器的定义用一个星号( * )来表示,使用 yield 关键字能指明在每次成功的 next() 方法调用时应当返回什么值。

  • 生成器委托促进了对迭代器行为的良好封装,让你能将已有的生成器重用在新的生成器中。通过调用 yield * 而非 yield ,你就能把已有生成器用在其他生成器内部。这种处理方式能创建一个从多个迭代器中返回值的新迭代器。

  • 迭代器是一个对象,拥有next()方法,方法返回一个对象,拥有value与done属性。迭代器持有一个指向集合位置的内部指针,每当调用了next方法,迭代器就会返回下一个值。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值