设计模式:迭代器模式

设计模式

wiki中将设计模式分为四类,分别是:

  • 创建模式(creational patterns)
  • 结构模式(structural patterns)
  • 行为模式(behavioral patterns)
  • 并发模式(concurrency patterns)

迭代器模式属于其中的行为模式。

迭代器做的只有一件事,就是解决集合的遍历问题。

以下是wiki对迭代器模式的概括性描述:

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

提供一种在不暴露底层表示的情况下按顺序访问聚合对象元素的方式。

这个描述乍一看有点抽象,但其实我们常用的for循环就可以看作是迭代器模式的应用,在遍历时并不知道数组元素的具体表示:

let array = [1, 2, 3, 4, 5, 6];
for (let i = 0; i < array.length; i ++) {
  console.log(array[i]);
}

假如不应用迭代器,我们想要遍历array数组就得像下面这么做:

console.log(array[0]);
console.log(array[1]);
console.log(array[2]);
// ...

但使用for循环的前提是array是数组,这样我们才能使用索引去遍历这个集合,所以上面这种for循环就是针对数组类型集合的一种迭代器实现。

定义

下面再进一步了解迭代器模式是什么:

In object-oriented programming, the iterator pattern is a design pattern) in which an iterator is used to traverse a container) and access the container's elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled.

For example, the hypothetical algorithm SearchForElement can be implemented generally using a specified type of iterator rather than implementing it as a container-specific algorithm. This allows SearchForElement to be used on any container that supports the required type of iterator.

在面向对象编程中,迭代器模式是一种设计模式,使用迭代器来遍历容器并访问容器中的元素。迭代器模式使算法与容器分离;在某些情况下,算法是特定于容器的,因此无法分离。

(可以看出,上面的for循环就是特定于容器,也就是数组类型的容器,同理,数组的原型方法forEach也是如此)

例如,假设算法'SearchForElement'可以使用特定类型的迭代器实现,而不是作为特定于容器的算法来实现。这样就可以在任何支持所需迭代器类型的容器上使用'SearchForElement'。

解决的问题

从上述定义中我们就可以知道,迭代器模式就是用于解决如何去遍历容器中的元素。

下面是wiki给出的描述:

  • The elements of an aggregate object should be accessed and traversed without exposing its representation (data structures).
  • New traversal operations should be defined for an aggregate object without changing its interface.

Defining access and traversal operations in the aggregate interface is inflexible because it commits the aggregate to particular access and traversal operations and makes it impossible to add new operations later without having to change the aggregate interface.

翻译过来即:

  • 访问和遍历聚合对象中的元素时,不应暴露其表示形式(数据结构)
  • 应在不改变聚合对象接口的情况下,为聚合对象定义新的遍历操作

在聚合接口中定义访问和遍历操作是不灵活的,因为这样做会使聚合对象服从于特定的访问和遍历操作,以后就不可能在不改变聚合接口的情况下添加新的操作。

(这段话的意思应该是指,访问和遍历操作应该由聚合对象自行实现)

如何做

那具体应该如何操作呢?

  • Define a separate (iterator) object that encapsulates accessing and traversing an aggregate object.
  • Clients use an iterator to access and traverse an aggregate without knowing its representation (data structures).

Different iterators can be used to access and traverse an aggregate in different ways.
New access and traversal operations can be defined independently by defining new iterators.

翻译过来即:

  • 定义一个单独的(迭代器)对象,封装对聚合对象的访问和遍历
  • 客户端使用迭代器来访问和遍历聚合对象,而无需了解其表示形式(数据结构)

不同的迭代器可以不同的方式访问和遍历聚合对象。

通过定义新的迭代器,可以独立定义新的访问和遍历操作。

ES6实现

在ES6之前,可以被遍历的对象严格来说只有数组,所以遍历方法都是针对数组这个集合类型;甚至类数组都不能直接调用forEach方法。

但现在E6中新增了Map和Set等集合类型,原来的遍历方法就更大地暴露出了它们的缺陷:

  • 遍历的对象必须是数组
  • 使用的是递增索引的方式

[Symbol.iterator]()

为此ES6定义了新的可迭代协议,即任何对象都可以通过定义一个迭代器接口,来生成迭代器,这个接口被称为“迭代器工厂”,即[Symbol.iterator](),来定义其被迭代的方式;此时这个类型便被称为“可迭代对象”(Iterable)。

迭代器可以提供给各种遍历方法调用,比如:for-of、数组解构、扩展操作符等操作,都会调用可迭代对象的迭代器接口得到其对应的迭代器,然后调用迭代器的next()方法来完成对可迭代对象的遍历,如果有提前结束遍历的需求,还可以给迭代器定义return()方法。

next()return()方法返回的对象被称为“IteratorResult对象”,包含有value和done两个属性,value表示迭代值,done表示遍历是否结束,当done的值为true时,代表遍历结束。

以下是一个简单的例子:

class Counter {
    constructor(limit) {
        this.limit = limit;
    }

   // 迭代器接口
    [Symbol.iterator]() {
        let count = 1,
          o = this;

        return { // 迭代器本体
          next() {
              if( count <= o.limit) {
                return { // IteratorResult对象
                   done: false,
                   value: count++
                };
              } else {
                return {
                   done: true
                 };
               }
           },
           return() {
               console.log( 'Exiting early' );
               return {
                  done: true
                };
            }
        }
   }
}

let counter1 = new Counter(5);
for (let i of counter1) {
    if(i > 2) {
        break;
    }
    console.log( i );
}

// 1
// 2
// Exiting early

在上述代码中,for-of在遍历过程中相当于调用迭代器的next()方法,当执行到break;语句时,就相当于调用了迭代器的return()方法;上述代码等价于:

// 通过调用迭代器工厂,获取迭代器对象
const iterator = counter1[Symbol.iterator]();
let iterator_result = { done: false };
while(!iterator_result.done) {
   iterator_result = iterator.next();
   if (iterator_result.value > 2) {
     iterator_result = iterator.return();
   } else {
     console.log(iterator_result.value);
   }
}

在ES6中,Array、Map和Set等集合类型都内置了单独的迭代器。

每个内置可迭代对象的默认迭代器接口所产生的迭代器,也有自己的默认迭代器函数,返回指向自身(并未严格实现Iterable接口,默认迭代器函数不能创建新的迭代器),即如下所示:

[Symbol.iterator]() {
    // ...

    return {
        next() {
           // ...
         },
        return() {
            // ...
        },
       [Symbol.iterator]() {
         return this;
       }
    }
}

Generator

另外,ES6还提供了生成迭代器的函数,简称“生成器(Generator)”,相当于迭代器工厂[Symbol.iterator]()这个接口,可以通过*来声明生成器函数,并配合yield来实现更灵活的遍历。

来看下面的例子:

function* generatorFn3() {
  // ...一些操作
  yield 'foo';
  // ...一些操作
  yield 'bar';
  // ...一些操作
  return 'baz';
}
let g3 = generatorFn3(); // 生成一个迭代器

此时通过迭代器来遍历,当执行第n次g3.next(),就会执行第n个yield(没有yield就是return)之前的代码,返回对象中的value即对应yield或return跟着的值,当执行到yield时,done为false,当执行到return,done就为true,如下所示:

console.log( g3.next() ); // { value: 'foo', done: false }
console.log( g3.next() ); // { value: 'bar', done: false }
console.log( g3.next() ); // { value: 'baz', done: true }

以上代码相当于:

g3 = generatorFn3();
for(const x of g3) {
    console.log( x );
}

但只会输出foo和bar,return后面跟着的值不属于迭代值。

总结

一般情况下,我们不需要自己实现迭代器,ES6内置的迭代器就足够日常的开发使用了,但了解迭代器模式,以及ES6提供的迭代器工厂和生成器函数,可以帮助我们解决特殊的开发需求,设计更灵活的代码方案(利用generator和yield)。

你可能感兴趣的:(设计模式:迭代器模式)