迭代器

迭代器(iterator) 是一种结构化的模式,用于从一个信息源中以一次一个的方式抽取信息。这种模式在程序设计中存在很久了。而且不可否认的是,不知从什么时候起JS开发者们就已经特别地设计并实现了迭代器,所以它根本不是什么新的话题。

ES6所做的是,为迭代器引入了一个隐含的标准化接口。许多在JavaScript中内建的数据结构现在都会暴露一个实现了这个标准的迭代器。而且你也可以构建自己的遵循同样标准的迭代器,来使互用性最大化。

迭代器是一种消费数据的方法,它是组织有顺序的,相继的,基于抽取的。

举个例子,你可能实现一个工具,它在每次被请求时产生一个新的唯一的标识符。或者你可能循环一个固定的列表以轮流的方式产生一系列无限多的值。或者你可以在一个数据库查询的结果上添加一个迭代器来一次抽取一行结果。

虽然在JS中它们不经常以这样的方式被使用,但是迭代器还可以认为是每次控制行为中的一个步骤。这会在考虑generator时得到相当清楚的展示(参见本章稍后的“Generator”),虽然你当然可以不使用generator而做同样的事。

接口

在本书写作的时候,ES6的25.1.1.2部分 (https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface) 详述了Iterator接口,它有如下的要求:

Iterator [必须]
    next() {method}: 取得下一个IteratorResult

有两个可选成员,有些迭代器用它们进行了扩展:

Iterator [可选]
    return() {method}: 停止迭代并返回IteratorResult
    throw() {method}: 通知错误并返回IteratorResult

接口IteratorResult被规定为:

IteratorResult
    value {property}: 当前的迭代值或最终的返回值
        (如果它的值为`undefined`,是可选的)
    done {property}: 布尔值,指示完成的状态

注意: 我称这些接口是隐含的,不是因为它们没有在语言规范中被明确地被说出来 —— 它们被说出来了!—— 而是因为它们没有作为可以直接访问的对象暴露给代码。在ES6中,JavaScript不支持任何“接口”的概念,所以在你自己的代码中遵循它们纯粹是惯例上的。但是,不论JS在何处需要一个迭代器 —— 例如在一个for..of循环中 —— 你提供的东西必须遵循这些接口,否则代码就会失败。

还有一个Iterable接口,它描述了一定能够产生迭代器的对象:

Iterable
    @@iterator() {method}: 产生一个迭代器

如果你回忆一下第二章的“内建Symbol”,@@iterator是一种特殊的内建symbol,表示可以为对象产生迭代器的方法。

IteratorResult

IteratorResult接口规定从任何迭代器操作的返回值都是这样形式的对象:

{ value: .. , done: true / false }

内建迭代器将总是返回这种形式的值,当然,更多的属性也允许出现在这个返回值中,如果有必要的话。

例如,一个自定义的迭代器可能会在结果对象中加入额外的元数据(比如,数据是从哪里来的,取得它花了多久,缓存过期的时间长度,下次请求的恰当频率,等等)。

注意: 从技术上讲,在值为undefined的情况下,value是可选的,它将会被认为是不存在或者是没有被设置。因为不管它是表示的就是这个值还是完全不存在,访问res.value都将会产生undefined,所以这个属性的存在/不存在更大程度上是一个实现或者优化(或两者)的细节,而非一个功能上的问题。

next()迭代

让我们来看一个数组,它是一个可迭代对象,可以生成一个迭代器来消费它的值:

var arr = [1,2,3];

var it = arr[Symbol.iterator]();

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

it.next();        // { value: undefined, done: true }

每一次定位在Symbol.iterator上的方法在值arr上被调用时,它都将生成一个全新的迭代器。大多数的数据结构都会这么做,包括所有内建在JS中的数据结构。

然而,像事件队列这样的结构也许只能生成一个单独的迭代器(单例模式)。或者某种结构可能在同一时间内只允许存在一个唯一的迭代器,要求当前的迭代器必须完成,才能创建一个新的。

前一个代码段中的it迭代器不会再你得到值3时报告done: true。你必须再次调用next(),实质上越过数组末尾的值,才能得到完成信号done: true。在这一节稍后会清楚地讲解这种设计方式的原因,但是它通常被认为是一种最佳实践。

基本类型的字符串值也默认地是可迭代对象:

var greeting = "hello world";

var it = greeting[Symbol.iterator]();

it.next();        // { value: "h", done: false }
it.next();        // { value: "e", done: false }
..

注意: 从技术上讲,这个基本类型值本身不是可迭代对象,但多亏了“封箱”,"hello world"被强制转换为它的String对象包装形式, 才是一个可迭代对象。更多信息参见本系列的 类型与文法

ES6还包括几种新的数据结构,称为集合(参见第五章)。这些集合不仅本身就是可迭代对象,而且它们还提供API方法来生成一个迭代器,例如:

var m = new Map();
m.set( "foo", 42 );
m.set( { cool: true }, "hello world" );

var it1 = m[Symbol.iterator]();
var it2 = m.entries();

it1.next();        // { value: [ "foo", 42 ], done: false }
it2.next();        // { value: [ "foo", 42 ], done: false }
..

一个迭代器的next(..)方法能够可选地接受一个或多个参数。大多数内建的迭代器不会实施这种能力,虽然一个generator的迭代器绝对会这么做(参见本章稍后的“Generator”)。

根据一般的惯例,包括所有的内建迭代器,在一个已经被耗尽的迭代器上调用next(..)不是一个错误,而是简单地持续返回结果{ value: undefined, done: true }

可选的return(..)throw(..)

在迭代器接口上的可选方法 —— return(..)throw(..) —— 在大多数内建的迭代器上都没有被实现。但是,它们在generator的上下文环境中绝对有某些含义,所以更具体的信息可以参看“Generator”。

return(..)被定义为向一个迭代器发送一个信号,告知它消费者代码已经完成而且不会再从它那里抽取更多的值。这个信号可以用于通知生产者(应答next(..)调用的迭代器)去实施一些可能的清理作业,比如释放/关闭网络,数据库,或者文件引用资源。

如果一个迭代器拥有return(..),而且发生了可以自动被解释为非正常或者提前终止消费迭代器的任何情况,return(..)就将会被自动调用。你也可以手动调用return(..)

return(..)将会像next(..)一样返回一个IteratorResult对象。一般来说,你向return(..)发送的可选值将会在这个IteratorResult中作为value发送回来,虽然在一些微妙的情况下这可能不成立。

throw(..)被用于向一个迭代器发送一个异常/错误信号,与return(..)隐含的完成信号相比,它可能会被迭代器用于不同的目的。它不一定像return(..)一样暗示着迭代器的完全停止。

例如,在generator迭代器中,throw(..)实际上会将一个被抛出的异常注射到generator暂停的执行环境中,这个异常可以用try..catch捕获。一个未捕获的throw(..)异常将会导致generator的迭代器异常中止。

注意: 根据一般的惯例,在return(..)throw(..)被调用之后,一个迭代器就不应该在产生任何结果了。

迭代器循环

正如我们在第二章的“for..of”一节中讲解的,ES6的for..of循环可以直接消费一个规范的可迭代对象。

如果一个迭代器也是一个可迭代对象,那么它就可以直接与for..of循环一起使用。通过给予迭代器一个简单地返回它自身的Symbol.iterator方法,你就可以使它成为一个可迭代对象:

var it = {
    // 使迭代器`it`成为一个可迭代对象
    [Symbol.iterator]() { return this; },

    next() { .. },
    ..
};

it[Symbol.iterator]() === it;        // true

现在我们就可以用一个for..of循环来消费迭代器it了:

for (var v of it) {
    console.log( v );
}

为了完全理解这样的循环如何工作,回忆下第二章中的for..of循环的for等价物:

for (var v, res; (res = it.next()) && !res.done; ) {
    v = res.value;
    console.log( v );
}

如果你仔细观察,你会发现it.next()是在每次迭代之前被调用的,然后res.done才被查询。如果res.donetrue,那么这个表达式将会求值为false于是这次迭代不会发生。

回忆一下之前我们建议说,迭代器一般不应与最终预期的值一起返回done: true。现在你知道为什么了。

如果一个迭代器返回了{ done: true, value: 42 }for..of循环将完全扔掉值42。因此,假定你的迭代器可能会被for..of循环或它的for等价物这样的模式消费的话,你可能应当等到你已经返回了所有相关的迭代值之后才返回done: true来表示完成。

警告: 当然,你可以有意地将你的迭代器设计为将某些相关的valuedone: true同时返回。但除非你将此情况在文档中记录下来,否则不要这么做,因为这样会隐含地强制你的迭代器消费者使用一种,与我们刚才描述的for..of或它的手动等价物不同的模式来进行迭代。

自定义迭代器

除了标准的内建迭代器,你还可以制造你自己的迭代器!所有使它们可以与ES6消费设施(例如,for..of循环和...操作符)进行互动的代价就是遵循恰当的接口。

让我们试着构建一个迭代器,它能够以斐波那契(Fibonacci)数列的形式产生无限多的数字序列:

var Fib = {
    [Symbol.iterator]() {
        var n1 = 1, n2 = 1;

        return {
            // 使迭代器成为一个可迭代对象
            [Symbol.iterator]() { return this; },

            next() {
                var current = n2;
                n2 = n1;
                n1 = n1 + current;
                return { value: current, done: false };
            },

            return(v) {
                console.log(
                    "Fibonacci sequence abandoned."
                );
                return { value: v, done: true };
            }
        };
    }
};

for (var v of Fib) {
    console.log( v );

    if (v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Fibonacci sequence abandoned.

警告: 如果我们没有插入break条件,这个for..of循环将会永远运行下去,这回破坏你的程序,因此可能不是我们想要的!

方法Fib[Symbol.iterator]()在被调用时返回带有next()return(..)方法的迭代器对象。它的状态通过变量n1n2维护在闭包中。

接下来让我们考虑一个迭代器,它被设计为执行一系列(也叫队列)动作,一次一个:

var tasks = {
    [Symbol.iterator]() {
        var steps = this.actions.slice();

        return {
            // 使迭代器成为一个可迭代对象
            [Symbol.iterator]() { return this; },

            next(...args) {
                if (steps.length > 0) {
                    let res = steps.shift()( ...args );
                    return { value: res, done: false };
                }
                else {
                    return { done: true }
                }
            },

            return(v) {
                steps.length = 0;
                return { value: v, done: true };
            }
        };
    },
    actions: []
};

tasks上的迭代器步过在数组属性actions中找到的函数,并每次执行它们中的一个,并传入你传递给next(..)的任何参数值,并在标准的IteratorResult对象中向你返回任何它返回的东西。

这是我们如何使用这个tasks队列:

tasks.actions.push(
    function step1(x){
        console.log( "step 1:", x );
        return x * 2;
    },
    function step2(x,y){
        console.log( "step 2:", x, y );
        return x + (y * 2);
    },
    function step3(x,y,z){
        console.log( "step 3:", x, y, z );
        return (x * y) + z;
    }
);

var it = tasks[Symbol.iterator]();

it.next( 10 );            // step 1: 10
                        // { value:   20, done: false }

it.next( 20, 50 );        // step 2: 20 50
                        // { value:  120, done: false }

it.next( 20, 50, 120 );    // step 3: 20 50 120
                        // { value: 1120, done: false }

it.next();                // { done: true }

这种特别的用法证实了迭代器可以是一种具有组织功能的模式,不仅仅是数据。这也联系着我们在下一节关于generator将要看到的东西。

你甚至可以更有创意一些,在一块数据上定义一个表示元操作的迭代器。例如,我们可以为默认从0开始递增至(或递减至,对于负数来说)指定数字的一组数字定义一个迭代器。

考虑如下代码:

if (!Number.prototype[Symbol.iterator]) {
    Object.defineProperty(
        Number.prototype,
        Symbol.iterator,
        {
            writable: true,
            configurable: true,
            enumerable: false,
            value: function iterator(){
                var i, inc, done = false, top = +this;

                // 正向迭代还是负向迭代?
                inc = 1 * (top < 0 ? -1 : 1);

                return {
                    // 使迭代器本身成为一个可迭代对象!
                    [Symbol.iterator](){ return this; },

                    next() {
                        if (!done) {
                            // 最初的迭代总是0
                            if (i == null) {
                                i = 0;
                            }
                            // 正向迭代
                            else if (top >= 0) {
                                i = Math.min(top,i + inc);
                            }
                            // 负向迭代
                            else {
                                i = Math.max(top,i + inc);
                            }

                            // 这次迭代之后就完了?
                            if (i == top) done = true;

                            return { value: i, done: false };
                        }
                        else {
                            return { done: true };
                        }
                    }
                };
            }
        }
    );
}

现在,这种创意给了我们什么技巧?

for (var i of 3) {
    console.log( i );
}
// 0 1 2 3

[...-3];                // [0,-1,-2,-3]

这是一些有趣的技巧,虽然其实际用途有些值得商榷。但是再一次,有人可能想知道为什么ES6没有提供如此微小但讨喜的特性呢?

如果我连这样的提醒都没给过你,那就是我的疏忽:像我在前面的代码段中做的那样扩展原生原型,是一件你需要小心并了解潜在的危害后才应该做的事情。

在这样的情况下,你与其他代码或者未来的JS特性发生冲突的可能性非常低。但是要小心微小的可能性。并在文档中为后人详细记录下你在做什么。

注意: 如果你想知道更多细节,我在这篇文章(http://blog.getify.com/iterating-es6-numbers/) 中详细论述了这种特别的技术。而且这段评论(http://blog.getify.com/iterating-es6-numbers/comment-page-1/#comment-535294)甚至为制造一个字符串字符范围提出了一个相似的技巧。

消费迭代器

我们已经看到了使用for..of循环来一个元素一个元素地消费一个迭代器。但是还有一些其他的ES6结构可以消费迭代器。

让我们考虑一下附着这个数组上的迭代器(虽然任何我们选择的迭代器都将拥有如下的行为):

var a = [1,2,3,4,5];

扩散操作符...将完全耗尽一个迭代器。考虑如下代码:

function foo(x,y,z,w,p) {
    console.log( x + y + z + w + p );
}

foo( ...a );            // 15

...还可以在一个数组内部扩散一个迭代器:

var b = [ 0, ...a, 6 ];
b;                        // [0,1,2,3,4,5,6]

数组解构(参见第二章的“解构”)可以部分地或者完全地(如果与一个...剩余/收集操作符一起使用)消费一个迭代器:

var it = a[Symbol.iterator]();

var [x,y] = it;            // 仅从`it`中取前两个元素
var [z, ...w] = it;        // 取第三个,然后一次取得剩下所有的

// `it`被完全耗尽了吗?是的
it.next();                // { value: undefined, done: true }

x;                        // 1
y;                        // 2
z;                        // 3
w;                        // [4,5]

results matching ""

    No results matching ""