Iterator由来

不推荐Iterator方法。 Iterator 函数是一个 SpiderMonkey 专有特性,并且会在某一时刻被删除。
有一点,需要清楚的,就是“迭代协议”。迭代协议MDN说明

// 简单示例,摘自“深入理解ES6”
function createIterator(items) {
    let i = 0;
    
    return {
        next: function() {
            let done = (i >= items.length);
            let value = !done ? items[i++] : undefined;
            
            return {
                done,
                value
            }
        }
    }
}
let iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }
// 之后所有的调用都会返回相同的内容
console.log(iterator.next()); // { done: true, value: undefined }

Generator定义

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

function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}
let iterator = createIterator();
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 *createIterator(items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
}

let iterator = createIterator([1, 2, 3]);
// iterator 与前面代码创建的 iterator 功能一样,可以试一下。
yield的使用限制
yield关键字只能在生成器内部使用,在其他地方使用会导致程序抛出语法错误,即便在生成器的函数里使用也如此。
function *createIterator(items) {
    items.forEach(function(item) {
        yield item + 1;
    });
}
// 会报语法错误 node ./iterator.js

从字面理解,yield关键字确定在createIterator()函数内部,但是它与return关键字一样,二者都不能穿透函数边界。嵌套函数中的return语句不能用作函数的返回语句,而此处嵌套函数中的yield语句会导致程序抛出语法错误。
生成器函数表达式&对象方法
通过上面的方法,关于函数表达式和对象方法,直接上代码吧,更明白。

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

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

// 对象方法
let o = {
    createIterator: function *(items) {
        yield items[i];
    }
}

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

可迭代对象 & for-of 循环

看过Symbol文章的小伙伴应该都知道,Symbol.iterator就是 well-known Symbol之一。可迭代对象就具有Symbol.iterator属性,它是一种与迭代器密切相关的对象。它通过指定的函数可以返回一个作用于附属对象的迭代器。在ES6中,所有的集合对象(Array, Set, Map)和字符串都是可迭代对象,这些对象中都有默认的迭代器。当然,ES中也添加了for-of循环这些可迭代对象。

  • 迭代器
  • for-of循环

这是解决循环内部索引跟踪问题的关键工具。
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方法来获取迭代器,这一过程是在Javascript引擎背后完成的。随后迭代器的next()的方法被多次调用,从其返回对象的value属性读取值并存储在变量num中,直到对象的done为true时循环退出,所以num不会赋值为undefined

访问默认迭代器

从上面的例子可以看出,可迭代对象都有一个默认迭代器。这个迭代器可通过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 isIterator(object) {
    return typeof object[Symbol.iterator] === "function";
}

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

创建可迭代对象

默认情况下,开发者定义的对象都是不可迭代的对象,但如果给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 item of collection){
    console.log(item);
}
// 输出:
// 1
// 2
// 3

内建迭代器

到这里,应该明白,ES6中已经默认为很多内建类型提供了内建迭代器,只有这些内容无法实现目标时,才需要自己创建。ES6中有三种集合对象: 数组、Map与Set集合,他们内建了以下三种迭代器:

  • entries() 返回值为多个键值对迭代器
  • values() 返回值为集合的迭代器
  • keys() 返回集合中所有键名迭代器

MDN 关于内建迭代器 说的比较简单,我就按“深入理解ES6”来详细说一下吧。

entries() 迭代器

特点:每次调用next()方法时,entries()返回一个数组,数组中两个元素,分别表示键和值。

- Array 第一个元素为数字类型的索引,第二个元素为值
- Set 第一个元素与第二个元素都是值,因为Set中值也作为键来使用
- Map 第一个元素为键,第二个元素为值
示例:
let colors = [ "red", "green", "blue"];
let tracking = new Set([ 123, 456, 890]);
let data = new Map();
data.set("title", "ES6之Iterator&Generator");
data.set("formate", "net share");

for (let entry of colors.entries()) {
    console.log(entry);
}
// 输出:
// [ 0, 'red' ]
// [ 1, 'green' ]
// [ 2, 'blue' ]

for (let entry of tracking.entries()) {
    console.log(entry);
}
// 输出:
// [ 123, 123 ]
// [ 456, 456 ]
// [ 890, 890 ]

for (let entry of data.entries()) {
    console.log(entry);
}
// 输出:
// [ 'title', 'ES6之Iterator&Generator' ]
// [ 'formate', 'net share' ]

values() 迭代器

特点: 返回集合中所存的所有值。
示例:

let colors = [ "red", "green", "blue"];
let tracking = new Set([ 123, 456, 890]);
let data = new Map();
data.set("title", "ES6之Iterator&Generator");
data.set("formate", "net share");

for (let entry of colors.values()) {
    console.log(entry);
}
// 输出:
// red
// green
// blue

for (let entry of tracking.values()) {
    console.log(entry);
}
// 输出:
// 123
// 456 
// 890 

for (let entry of data.values()) {
    console.log(entry);
}
// 输出:
// ES6之Iterator&Generator
// net share

keys() 迭代器

特点:返回集合中存在的每一个键。
示例:

    let colors = [ "red", "green", "blue"];
    let tracking = new Set([ 123, 456, 890]);
    let data = new Map();
    data.set("title", "ES6之Iterator&Generator");
    data.set("formate", "net share");
    
    for (let entry of colors.keys()) {
        console.log(entry);
    }
    // 输出:
    // 0
    // 1
    // 2
    
    for (let entry of tracking.keys()) {
        console.log(entry);
    }
    // 输出:
    // 123
    // 456
    // 890
    
    for (let entry of data.keys()) {
        console.log(entry);
    }
    // 输出:
    // title
    // formate

不同集合,会使用不同的默认迭代器,Array和Set使用的是values()迭代器,而Map则使用的是entries()迭代器。

string 迭代器

特点: 与Array类似。string也可以通过下标访问字符内容。由于下标操作是编码单元而非字符,所以无法访问双字节符。在Unicode支持不好的版本(node或浏览器引擎),会出现错误。如果使用for-of,则不会出现这种问题,因为其操作的是字符而非编码单元。
思考一个问题:展开运算符与非数组可迭代对象
如果对可迭代对象使用展开运算,会调用默认迭代器吗?返回又是什么呢?
非数组可迭代对象呢?
示例:

let o = {
    a: 'aaaa',
    b: 123,
    c: 'ddd',
    *[Symbol.iterator]() {
        yield this.a;
        yield this.b;
        yield this.c;
    }
}

let ar = [...o];
console.log(ar);
for(let item of ar) {
    console.log(item);
}
// 输出
// [ 'aaaa', 123, 'ddd' ]
// aaaa
// 123
// ddd

迭代器高级功能

给迭代器传参

这次先做示例,再做说明。
示例1:

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

let iterator = createIterator();
 console.log(iterator.next());
 console.log(iterator.next(4));
 console.log(iterator.next(6));
 console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: 6, done: false }
// { value: 9, done: false }
// { value: undefined, done: true }

示例2:

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

let iterator = createIterator();
 console.log(iterator.next());
 console.log(iterator.next(4));
 console.log(iterator.next(6));
 console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: 6, done: false }
// { value: 7, done: false }
// { value: undefined, done: true }

示例3:

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

let iterator = createIterator();
 console.log(iterator.next(5));
 console.log(iterator.next(4));
 console.log(iterator.next(6));
 console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: 6, done: false }
// { value: 9, done: false }
// { value: undefined, done: true }

示例4

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

let iterator = createIterator();
 console.log(iterator.next(5));
 console.log(iterator.next(4));
 console.log(iterator.next(6));
 console.log(iterator.next());
// 示例:
// { value: 1, done: false }
// { value: 3, done: false }
// { value: 9, done: false }
// { value: undefined, done: true }

示例5:

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

let iterator = createIterator();
 console.log(iterator.next());
 console.log(iterator.next());
 console.log(iterator.next());
 console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: NaN, done: false }
// { value: NaN, done: false }
// { value: undefined, done: true }

示例5的输出结果,是不是很意外?yield返回变量与正常变量赋值有何不同?
分析:

  • 传递参数会替代上一次yield的返回值
  • 第一个next()执行,传参无效。因为第一次调用yield时,之前没有任何yield语句执行
  • 非yield返回值 ,不受next()参数影响
  • 在一个含参的yield语句中,表达式右侧等价于第一次调用next()方法后下一个返回值。表达式左侧等价于第二次调用next()方法后,在函数继续执行前得到的返回值。

抛出错误

这个也先示例,后说明。
示例1:

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

let iterator = createIterator();
 console.log(iterator.next());
 console.log(iterator.next(4));
 console.log(iterator.throw(new Error('Boom')));
 console.log(iterator.next(5));
// 输出:
// { value: 1, done: false }
// { value: 6, done: false }
// xx/iterator.js:146
//    let second = yield first + 2;
//                 ^
//
// Error: Boom

分析:前两个表达式正常求值,在继续执行let second 求值前,错误就会被抛出,并阻止了代码继续执行。
这个过程,与直接抛出异常很相似,只是抛出的时机不同。
示例2

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

let iterator = createIterator();
 console.log(iterator.next());
 console.log(iterator.next(4));
 console.log(iterator.throw(new Error('Boom')));
 console.log(iterator.next(5));

// 输出:
// { value: 1, done: false }
// { value: 6, done: false }
// { value: 9, done: false }
// { value: undefined, done: true }

分析:
用try...catch语句来捕获异常,包裹着第二名语句。尽管这条语句本身没有错误,但在给second赋值前,还是会抛出错误,catch代码块捕捉到这个错误后,并把second = 6. 下一条yield语句继续执行后,返回9.

返回语句

基本上两个示例可以概括。
示例1

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

let iterator = createIterator();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: undefined, done: true }
// { value: undefined, done: true }

示例2

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

let iterator = createIterator();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: 33, done: true }
// { value: undefined, done: true }

委托生成

先看三个示例吧,这个只是语法规范。
示例1

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

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

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

var iterator = createCombinedIterator();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

// 输出:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 'red', done: false }
// { value: 'green', done: false }
// { value: true, done: false }
// { value: undefined, done: true }

分析:语法规范,多个迭代器合并,可以创建一个生成器,再给yield语句添加一个号,就可以将生成数据的过程委托给其他迭代器。当定义这些生成器时,只需要将号放置在关键字yield和生成器的函数名之间即可。
示例2

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

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

function *createCombinedIterator() {
    let result = yield *createNumIterator();
    yield *createRepeatIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// 输出 :
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 'repeat', done: false }
// { value: 'repeat', done: false }
// { value: 'repeat', done: false }
// { value: undefined, done: true }

分析:执行过程,先被委托给了createNumIterator(),返回值会被赋给变量result,执行到return 3时,返回精数值3。这个值随后被传入createRepeatIterator()方法。
但是,无论通过何种方式调用next()方法,数值3永远不会被返回,它只存在于createCombinedIterator()的内部。如果要输出3,参看示例3。
示例3


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

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

function *createCombinedIterator() {
    let result = yield *createNumIterator();
    yield result;
    yield *createRepeatIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// 输出:
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 'repeat', done: false }
// { value: 'repeat', done: false }
// { value: 'repeat', done: false }
// { value: undefined, done: true }

期待的3,出来了。

异步任务执行

生成器支持在函数中暂停代码执行,可以挖掘异步处理的更多方法。

简单任务执行器

示例:

function run(taskDef) {

    // 创建迭代器
    let task = taskDef();
    // 开始执行任务
    let result = task.next();

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

    step();
}

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

分析:

  • 函数run()接受一个生成器作为参数,这个函数定义了后续要执行的任务,生成一个迭代器并将它存储在变量task中。
  • 首次调用next(),返回的结果被存储起来,稍后继续使用。
  • step()函数检查result.done的值,false时,继续执行next()方法,并执行step()操作。
  • 每次执行next()返回值会覆盖变量result原来的值

传参

示例:

function run(taskDef) {

    // 创建迭代器
    let task = taskDef();
    // 开始执行任务
    let result = task.next();

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

    step();
}

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

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

注意yield表达式求值。

异步

示例:

// redux-saga 经典应用

redux-saga
Iterator&Generator 之 MDN 说明


anleo
30 声望1 粉丝

准备写es6系列、设计模式(node,javascript)、前端工程化、TDD&BDD(Reactjs&angularjs)