你不知道的迭代器

115 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

前言

在 ES6 中,数组原型提供了很多迭代方法,someeveryfilterforEachmap,还有归并方法 reduce,本质上也是迭代、这些迭代方法往往能满足我们的日常开发,但是在有些时候,并不能总是这么有效

除了数组之外的其他任意数据类型是否都能迭代呢?

能否自定义迭代方式呢?

ES6 中出现了 迭代器 / 生成器 能够很好的解决这些问题

1. 迭代器模式

我们首先从迭代器模式入手,详细地介绍迭代器

迭代器的种类分为内部迭代器外部迭代器

1.1 内部迭代器

在内部已经定义好了迭代规则,它完全控制整个迭代过程,外部只需要一次初始调用

type hasLengthProperty = {
  [index: number]: any;
  length: number;
};

function each<T extends hasLengthProperty>(
  iterator: T, // 迭代的对象可以是任何具有 length 属性的内容
  fn: (item: keyof T, index: number) => void
) {
  for (let i = 0; i < iterator.length; i++) {
    fn(iterator[i], i);
  }
}

使用

each(
  {
    '0': 0,
    '1': 1,
    '2': 2,
    length: 3,
  },
  function (item, index) {
    console.log('item:' + item, 'index:' + index);
  }
);

// item:0 index:0
// item:1 index:1
// item:2 index:2

这种迭代方式在调用的时候非常简单,外界不用关心迭代器内部的实现,但是如果同时迭代两个数组,这种方式就捉襟见肘了。比如判断两个数组是否全等,只能在 each 提供的接口 回调函数 中判断每一项是否全等。

因此,第二种方法,外部迭代器就应运而生。

1.2 外部迭代器

可以显示地执行迭代下一个元素

type hasLengthProperty = {
  [index: number]: any;
  length: number;
};

class Iterator<T extends hasLengthProperty> {
  iterator: T;
  currIdx: number = 0;

  constructor(iterator: T) {
    this.iterator = iterator;
  }

  next() {
    const item = this.iterator[this.currIdx++];

    return {
      value: item,
      done: item ? false : true, // 迭代完所有元素之后就可以标识迭代进程的结束
    };
  }
}

使用:

const iterator = new Iterator([1, 2, 3]);

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

通过这个例子可以很好的看出,外部迭代器的优势,将迭代的权利反转交给外部,由外部控制整个迭代的过程

外部迭代器对比内部迭代器还有一个很大的优势,当需要终止整个迭代过程,外部迭代器可以很简单地实现,而内部迭代器需要在迭代的地方手动 break 或者 return,在一些 es6 内置的数组方法中,不能通过这两个关键字终止迭代,只能 throw Errortry ~ catch

try {
  [1, 2, 3, 4].forEach(item => {
    if (item === 3) {
      throw new Error('终止迭代');
    }
    console.log(item);
  });
} catch (e: any) {
  console.log(e.message);
}

// 1
// 2
// 终止迭代

2. ES6 的迭代器

2.1 迭代器

定义:实现了 next 方法,在 next 方法里实现了迭代的过程

就像上面的例子,我们可以对任意一种数据结构自定义迭代规则,封装在 next 方法里,拥有 next 方法控制迭代过程的对象我们就可以说它是迭代器

2.2 可迭代对象

定义:实现了可迭代协议的对象。包含的元素都是有限的,而且都具有无歧义的遍历顺序

迭代器的区别:迭代器可以看作是可迭代对象构造出来的结果 image.png

可迭代协议

  • 实现了[Symbol.iterator]为key的方法,且这个方法返回了一个 迭代器对象,也就是实现了 next 方法的对象

一些常见的可迭代对象

  • 数组
  • Set
  • Map
  • 类数组:字符串、arguments、NodeList

而这些可迭代对象具有相同的特性

  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合(能被 Set new)
  • 创建映射(能被 Map new)
  • Promise.all()接收由期约组成的可迭代对象
  • Promise.race()接收由期约组成的可迭代对象
  • yield*操作符,在生成器中使用

字符串也是可迭代对象

[...'123456'];
// ['1', '2', '3', '4', '5', '6']

在可迭代对象调用迭代器工厂函数

[][Symbol.iterator] // 迭代器工厂函数
// ƒ values() { [native code] }

[][Symbol.iterator]() // 返回迭代器

执行迭代器工厂函数返回结果是迭代器: 具有 next 方法 image.png

可迭代对象和迭代器通过 Symbol.iterator 联系在一起,当我们调用 next 方法返回的对象 {value: any; done: boolean} value 包含可迭代对象的下一个值(done 为 false),或者 undefined(done 为 true),表示迭代过程的结束

2.3 自定义迭代器

安装上文的讲述,我们知道了什么迭代器,那我们可以根据这个规范自定义迭代器

const obj = {
  value: '123456',
  [Symbol.iterator]: function () {
    let index = 0;
    return {
      next: () => {
        const value: string | undefined = this.value[index++];

        return { value, done: value ? false : true };
      },
    };
  },
};

for (const i of obj) {
  console.log(i);
}
// 1
// 2
// 3
// 4
// 5
// 6

这些可迭代对象具有共同的特性,比如 for~of 循环... 扩展运算本质都是调用了可迭代对象下面的 Symbol.iterator 方法,隐式执行了 next 方法的过程

封装 Iterator

type hasLengthProperty = {
  [index: number]: any;
  length: number;
};

class Iterator<T extends hasLengthProperty> {
  private iterator: T;

  constructor(iterator: T) {
    this.iterator = iterator;
  }

  [Symbol.iterator]() {
    let index = 0;

    return {
      next: () => {
        let value = this.iterator[index++];
        return {
          value,
          done: value ? false : true,
        };
      },

      [Symbol.toStringTag]: 'Iterator',
    };
  }
}

for (const i of new Iterator([1, 2, 3, 4])) {
  console.log(i);
}
// 1
// 2
// 3
// 4

for~of 循环会忽略掉 done: true 的内容,在这个例子中封装方法中并不完美,如果传入内容的某一项元素为 undefined | null | '' | false | NaN 则会终止迭代,请读者自行修改,可以从 index 和 length 这个角度做文章

注意Symbol.iterator 属性引用的工厂函数 会返回相同的迭代器

const arr: number[] = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();

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

for (const i of iterator) {
  console.log(i);
}
// 1
// 2
// 3
// 4

这个例子还说明迭代器中依然内置了可迭代协议 Symbol.iterator image.png

改写 Iterator

class Iterator<T> {
  ...
  [Symbol.iterator](): any {
    ...
    
    return {
      ...,
      [Symbol.iterator]: Iterator.prototype[Symbol.iterator].bind(this),
    }
  }
}

我们在这里给每个迭代器对象都添加了可迭代协议,也就是说,自定义迭代器也是可迭代对象

const iterator = new Iterator([1, 2, 3])
  [Symbol.iterator]()
  [Symbol.iterator]()
  [Symbol.iterator]()
  [Symbol.iterator]()
  [Symbol.iterator]()
  [Symbol.iterator]();

for (const i of iterator) {
  console.log(i);
}
// 1
// 2
// 3

2.4 终止迭代器

迭代器对象中可选的 return方法用于指定在迭代器提前关闭时执行的逻辑。

我们在自定义迭代器中添加 return 方法

class Iterator<T> {
  ...
  [Symbol.iterator](): any {
    ...
    
    return {
      ...,
      return() {
        console.log('终止迭代');
        return { done: true, value: undefined };
      },
    }
  }
}   

可以关闭迭代器的情况可能为:

  • for-of 循环通过 breakcontinuereturnthrow 提前退出;
  • 未使用完的解构对象中的元素
const iterator1 = new Iterator([1, 2, 3])[Symbol.iterator]();
const iterator2 = new Iterator([1, 2, 3])[Symbol.iterator]();
const iterator3 = new Iterator([1, 2, 3])[Symbol.iterator]();

// 1.
const [a, b] = iterator1;
// 输出'终止迭代',会触发 return 方法

// 2.
for (const i of iterator2) {
  if (i > 1) {
    break;
  }
  console.log(i);
}
// 1
// 终止迭代

// 3.
try {
  for (const i of iterator3) {
    if (i > 1) {
      throw new Error();
    }
    console.log(i);
  }
} catch (e) {}
// 1
// 终止迭代

如上面的代码所示,还有一些内置的可迭代对象也会触发对应的 return 方法

const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();

iter.return = function () {
  console.log('终止迭代');
  return { done: true, value: undefined };
};

for (const i of iter) {
  if (i > 2) {
    break;
  }

  console.log(i);
}
// 1
// 2
// 终止迭代

如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。比如,数组的迭代器就是不能关闭的,但是可以终止迭代:

const iterator = [1, 2, 3][Symbol.iterator]();

iter.return = function () {
  console.log('终止迭代');
  return { done: true, value: undefined };
};

for (const i of iterator) {
  if (i > 1) {
    break;
  }
  console.log(i);
}
// 1
// 终止迭代

for (const i of iterator) {
  console.log(i);
}
// 3

因为 return方法是可选的,所以并非所有迭代器都是可关闭的。不过,仅仅给一个不可关闭的迭代器增加这 个方法并不能让它变成可关闭的。这是因为调用 return 不会强制迭代器进入关闭状态。即便如此, return 方法还是会被调用。

而我们自定义的 Iterator 在这里表现与这些内置方法有很大的差异

const iterator = new Iterator([1, 2, 3])[Symbol.iterator]();
iterator.return = function () {
  console.log('终止迭代');

  return { done: true, value: false };
};

for (const i of iterator) {
  if (i > 1) {
    break;
  }
  console.log(i);
}
// 1
// 终止迭代

for (const i of iterator) {
  console.log(i);
}
// 1
// 2
// 3

相同的迭代器执行过程独立,因为我们自制的迭代器会在每一次执行 for~of 的时候会重新调用 [Symbol.iterator]index 为两个不同的变量

3. 总结

一个任何类型的数据结构只要添加了 [Symbol.iterator],并且满足可迭代协议,那我们就可以改造不可迭代的数据结构变为可迭代的对象