ES6中的迭代器(Iterator)与生成器(Generator)

迭代器和生成器 iterators and generators

  • 转变: 迭代集合中的数据不需要再使用初始化变量作为索引的for循环,转而使用iterators对象来程序化的返回集合中下一个位置的项。
  • 优点: iterators使得集合的操作变得更容易
  • 高效处理数据的关键: 迭代器,数组方法,新增的集合类型(set,map)结合使用
  • es6中iterators的身影: for-of循环, 扩展(…)运算符

the loop problem

let colors = ['red','yellow'];
for (let i = 0; i < colors.length; i++) {
  const element = colors[i];
}

缺点: 循环嵌套之后复杂度就会增加,同时需要追踪多个索引变量。

what are iterators

迭代器是带有特殊接口的对象来程序化的返回下一位置的项。

所有的迭代器对象都带有next()方法,并返回一个包含两个属性的结果对象。
{
value: 代表下一个位置的值
done: 下面是否还有值可供迭代 true/false
}

原理:迭代器使用内部指针,来指向集合中某个值的位置,next()方法被调用时,返回下一位置的值。

例子: 在 ECMAScript 5 中创建一个迭代器:

function createInterator(items) {
  var i = 0;
  return {
    next: function() {
      var done = i >= items.length;
      var value = !done ? items[i++] : undefined;
      return {
        done: done,
        value: value 
      };
    }
  };
}
var interator = createInterator([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 all further calls
console.log(iterator.next());           // "{ value: undefined, done: true }"

以上示例中,根据 ECMAScript 5 规范模拟实现的迭代器还是有些复杂。

幸运的是,ECMAScript 6 中还提供了生成器,简化了迭代器对象创建的过程。

what are generators

生成器是返回迭代器的函数。

特点: function * ,同时还使用yield关键字。

原理:当执行流遇到yield语句时,该生成器就停止运转了,不会执行其他任何部分的代码,指导迭代器再次调用next()方法,yield再执行。

// 生成器  同样一个例子,使用生成器来创建迭代器就显得很简洁了
function* createInterator(items) {
  for (let i = 0; i < items.length; i++) {
    yield items[i];
  }
}
// 调用生成器类似于调用函数,但是前者返回一个迭代器
var interator = createInterator([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 all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"

在该例子中,createInterator生成器函数被传入一个数组。在函数内部,一个循环正在执行并且把数组中的值返回给迭代器。当遇到yield时,循环就会停止,每次调用next()时,循环会继续执行直到再次遇到yield语句。

注意: yield 只能用在生成器内部。yield语句是无法跨越函数边界的。

// 生成器
function* createInterator(items) {
  items.forEach(function(item){
    // 语法错误
    yield item + 1;
  });
}

生成器的不同创建方法

 生成器函数表达式 generator function expressions

let createInterator = function* (items) {
  for (let i = 0; i < items.length; i++) {
    yield items[i];
  }
}
let iterator = createIterator([1, 2, 3]);

注意:无法使用箭头函数来创建生成器。

对象中的生成器方法 generator object methods

let o = {
  createInterator:function* (items) {
    for (let i = 0; i < items.length; i++) {
      yield items[i];
    }
  },
  // 或者
  *createInterator(items) {
    for (let i = 0; i < items.length; i++) {
      yield items[i];
    }
  }
}
let iterator = o.createIterator([1, 2, 3]);

可迭代类型和for-of (iterables and for-of)

可迭代类型介绍

可迭代类型是指包含Symbol.iterator 属性的对象。

属于可迭代类型的对象: 数组,set,map,字符串 , 因为他们都有默认的迭代器

目的: 可迭代类型是为了es6新增的for-of循环而设计的

所有由生成器创建的迭代器都是可迭代类型,因为生成器在默认情况下会自赋值给 Symbol.iterator 属性。

for-of 介绍

优点: 完全不需要在集合中追踪索引,让你更专注于集合内容的操作

原理:for-lof 循环会在可迭代类型每次迭代执行后调用 next() 并将结果对象存储在变量中。循环会持续进行直到结果对象的 done 属性为 true。

let values = [1,2,3];

for (const num of values) {
  console.log(num);
}
// 依次输出 1 2 3 

过程: for-of循环会调用values数组的Symbol.iterator方法来获取迭代器iterator(Symbol.iterator 方法由幕后的 JavaScript 引擎调用)。之后再调用iterator.next()并将结果对象中的value属性值依次赋给num变量。当检测到结果对象中的done为true时,循环退出。

结论: 如果你只想简单的迭代数组或集合中的元素,那么 for-of 循环比 for 要更好。for-of 一般不容易出错,因为要追踪的条件更少。所以还是把 for 循环留给复杂控制条件的需求吧。

访问默认的迭代器 accessing the default iterator

你可以使用Symbol.iterator来访问对象默认的迭代器,like this:

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 定义了默认的迭代器,你可以如下使用它来确定一个对象是否可迭代:

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

isIterator() 函数可以简单的查看对象是否有默认的并且类型为函数的迭代器。for-of在执行前也会做相似的检查。

创建可迭代类型 creating iterables

情景: 开发者自定义的对象默认是不可迭代类型,但是你可以为他们创建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

现在你已经见识了数组默认迭代器的用法,然而 ECMAScript 6 还内置了许多迭代器使得操作集合中的数据更加轻松。

内置的迭代器 built-in iterators

集合迭代器 collection iterators

ECMAScript 6 内置了三种类型的集合对象:数组,map 和 set 。它们都有如下内置的迭代器供你浏览数据。

  • entries() 返回一个数据集为集合中的键值对的迭代器
  • values() 。。。。。。。。。。。。值的。。。。。
  • keys() 。。。。。。。。。。。。键的。。。。。
entries() 迭代器

规则:
- 数组 键是索引
- set 键和值相同
- map 正常返回每一项

let data = new Map();

data.set("title", "hello");
data.set("content", "nihao");

for (const entry of colors.entries()) {
  console.log(entry);
}
for (const entry of tracking.entries()) {
  console.log(entry);
}
for (const entry of data.entries()) {
  console.log(entry);
}

输出:

[0, "red"]
[1, "yellow"]
[12, 12]
[23, 23]
["title", "hello"]
["content", "nihao"]
values() 迭代器

返回集合中每一项的值。

let color = ["red", "yellow"];
let tracking = new Set([12, 23]);
let data = new Map();

data.set("title", "hello");
data.set("content", "nihao");

for (const value of colors.values()) {
  console.log(value);
}
for (const value of tracking.values()) {
  console.log(value);
}
for (const value of data.values()) {
  console.log(value);
}

输出:

"red"
"yellow"
12
23
"hello"
"nihao"
keys() 迭代器

返回集合中每一项的键。

let color = ["red", "yellow"];
let tracking = new Set([12, 23]);
let data = new Map();

data.set("title", "hello");
data.set("content", "nihao");

for (const key of colors.keys()) {
  console.log(key);
}
for (const key of tracking.keys()) {
  console.log(key);
}
for (const key of data.keys()) {
  console.log(key);
}

输出:

0
1
12
23
"title"
"content"
集合类型的默认迭代器 default iterators for collection types

规则:
- 数组和set默认values()方法
- map默认entries() 方法

let color = ["red", "yellow"];
let tracking = new Set([12, 23]);
let data = new Map();

data.set("title", "hello");
data.set("content", "nihao");

// 等效于调用 colors.values()
for (const value of colors) {
  console.log(value);
}
// 等效于调用 tracking.values()
for (const value of tracking) {
  console.log(value);
}
// 等效于调用 data.entries()
for (const entry of data) {
  console.log(entry);
}

输出:

"red"
"yellow"
12
23
["title", "hello"]
["content", "nihao"]

解构与for-of循环 destructuring and for-of loops Map 构造函数的默认行为有助于在 for-of 循环中使用解构。如下所示:

let data = new Map();
data.set("title", "hello");
data.set("content", "nihao");

for (const [key, value] of data) {
  console.log(key + "=" + value);
}

字符串迭代器 string iterators

ECMAScript 5原理:对字符串启用了方括号语法来访问字符(例如,text[0] 可以获得该字符串中的首个字符,等等)。

方括号语法访问的是编码单元(code unit)而非字符本身,所以当获取双字节字符时会有意想不到的结果。

双字节字符会被当做两个编码单元对待。

var message = "A ð ®· B";
for (let i = 0; i < message.length; i++) {
  console.log(message[i]);
}

输出:

A
(blank)
(blank)
(blank)
(blank)
B

ECMAScript 6: 目标是完全支持 Unicode,所以字符串的默认迭代器就是为了解决字符的迭代问题而做的努力。

字符串默认迭代器作用的是字符本身而非编码单元。

var message = "A ð ®· B";
for (const c of message) {
  console.log(c);
}

输出:

A
(blank)
ð ®·
(blank)
B

NodeList的迭代器 NodeList iterators

文档对象模型(DOM)中包含了一个 NodeList 类型用来表示一些 DOM 元素的集合。

在ECMAScript6中使用for-of循环或在任何对象默认的迭代器的内部来迭代NodeList

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

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

扩展运算符和非数组可迭代类型 the spread operator and nonarray iterables

使用扩展运算符将 set 转换为数组:

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

console.log(array);

使用扩展运算符将 map 转换为数组:

let map = new Map([['name','kk'],['age',23]]),
    array = [...map];
console.log(array);

既然扩展运算符可以用在任意的可迭代类型上,那么它就成为了将可迭代类型转换为数组最简单的办法。你可以将字符串和浏览器中的 NodeList 对象分别转换为包含字符(不是编码单元)或 DOM 节点的数组。

迭代器高级用法 advanced iterator functionality

向迭代器传入参数 passing arguments to iterators

原理: 当你向next方法传入参数时,生成器使用该参数作为yield语句的值。

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

let iterator = createIterator();

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

首次调用next()有些特殊,传给他的任何参数都会被忽略。因为传给next()的参数是作为已返回的yield语句的值,那么首次调用传给 next() 的参数必须要在返回首个 yield 语句之前可供访问。显然这是不可能的,所以没有理由给首次调用的 next() 方法传参。

如果考虑生成器函数内部在每次运行时都执行了哪些代码,思路可能会清晰一些。图片用颜色区分了每次 yield 之前代码的执行情况。

ES6中的迭代器(Iterator)与生成器(Generator)_第1张图片

在迭代器中抛出错误 throwing errors in iterators

function* createIterator() {
  let first = yield 1;
  let second = yield first + 2; // 4 + 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"))); // 由生成器抛出错误

这种中断代码执行的行为类似于直接抛出错误

ES6中的迭代器(Iterator)与生成器(Generator)_第2张图片

使用try-catch捕捉错误

function* createIterator() {
  let first = yield 1;
  let second;
  try {
    second = yield first + 2; // yield first + 2 throw error 并且迭代器继续执行
  } catch (error) {
    second = 6; // 捕捉到错误,给second赋其他值
  }
  yield second + 3;
}

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() 指示迭代器继续执行的同时并抛出错误。至于这些指令如何处理,这就由生成器内部的代码来决定。

包含return语句的生成器 generator return statements

作用:使用return语句让生成器函数提前执行完毕并针对next的调用来指定一个返回值。

原理:生成器会将return语句的出现判断为所有任务已经处理完毕,done被赋值为true。

function* createGenerator() {
  yield 1;
  yield 2;
  return;
  yield 3; // 不可达的
}

let iterator = createGenerator();

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

指定一个返回值来赋给结果对象中的value属性:例如:

function* createGenerator() {
  yield 1;
  yield 2;
  return 'i`m back'; // 指定的返回值
  yield 3; // 不可达的
}

let iterator = createGenerator();

iterator.next(); // { value: 1, done : false}
iterator.next(); // { value: 2, done : false}
iterator.next(); // { value: 'i`m back', done : true}

iterator.next(); // { value: undefined, done : true}
// 任何指定的返回值都只能被结果对象使用一次。所以value为undefined

生成器代理 delegating generators

用法: 生成器可以使用 yield 和星号(*)这种特殊形式来代理其它生成器。

例如:

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

function* createColorGenerator() {
  yield "red";
  yield "yellow";
}

function* createCombinedGenrator() {
  yield* createNumberGenerator;
  yield* createColorGenerator;
  yield true;
}

let iterator = createCombinedGenrator();

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

从迭代器返回的值来看,他等价于只使用了一个迭代器并返回了所有的值。

进一步使用生成器返回的值处理复杂的任务,例如:

function* createNumberGenerator() {
  yield 1;
  return 2;
}

function* createRepeatGenerator(count) {
  for (let i = 0; i < count; i++) {
    yield `repeat${count}`    
  }
}

function* createCombinedGenrator() {
  let result =  yield* createNumberGenerator;
  yield result; // 2  显式添加了额外的 yield 语句来输出 createNumberIterator() 生成器的返回值。
  yield* createRepeatGenerator(result);
  yield true;
}

let iterator = createCombinedGenrator();

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

运行异步任务 asynchronous task running

异步操作的传统做法是在它结束之后调用回调函数。例如,考虑如下 Node.js 读取文件的代码:

let fs = require("fs");

fs.readFile("readme.md", function(error, res) {
  if (error) throw error;
  doSomethingWith(res);
  console.log("done");
});

一个简单的任务运行器 a simple task runner

因为 yield 可以中断执行,并在继续运行之前等待 next() 方法的调用,你可以不使用回调函数来实现异步调用。首先,你需要一个函数来调用生成器以便让迭代器开始运行,例如这样:

function runTask(task) {
  // 创建一个迭代器
  let taskIterator = task();

  // 任务开始运行,并且把返回的结果赋值给result
  let result = taskIterator.next();

  // 递归函数持续调用next方法直到result的返回值的done为true就停止递归
  function step() {
    if (!result.done) {
      result = taskIterator.next();
      step();
    }
  }
  // 开始递归执行任务
  step();
}

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

附加数据的任务运行器 task running with data

function runTask(task) {
  let taskIterator = task();

  let result = taskIterator.next();

  function step() {
    if (!result.done) {
      // 让数据在yield之间流动只需要给next传入参数
      result = taskIterator.next(result.value);
      step();
    }
  }
  step();
}

runTask(function *() {
  let value = yield 1;
  console.log(value); // 1
  value = yield value + 3;
  console.log(value); // 4
});

异步任务运行期 asynchronous task runner

标识异步操作的方法:

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

可以使用延迟函数的执行以将它改造为异步函数,例如:

function fetchData() {
  return function(callback) {
    setTimeout(() => {
      callback(null, "hi");
    }, 30);
  };
}

将任务运行器改造成包含异步操作的函数,例如:

function runTask(task) {
  // 创建迭代器
  let taskIterator = task();
  // 任务开始执行
  let result = taskIterator.next();
  // 递归调用next
  function step() {
    // 如果任务未完成
    if (!result.done) {
      if (typeof result.value === "function") {
        result.value(function(err, data) {
          if (err) result = task.throw(err);
          result = taskIterator.next(data);
          step();
        });
      } else {
        result = taskIterator.next(result.value);
        step();
      }
    }
  }
  // 开始递归
  step();
}

这样这个任务运行器就做好了处理异步任务的准备了。再来看读取文件:

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

readfile() 方法接收 filename 参数,并返回一个内部调用回调函数的函数。该回调函数会直接传给 fs.readFile() 方法并在异步方法结束后执行。你可以像下面这样使用 yield 来运行这个任务:

runTask(function*() {
  let contents = yield readFile("readme.md");
  doSomthingWith(contents);
  console.log("done");
});

优点:在灭于显示的书写回调函数的同时实现了异步的readFile操作。

只要包含异步操作的函数和上述 fs.readFile() 接口一致,你就可以使用该例来书写从视觉上认为是同步的逻辑。

参考书籍:《Understanding ECMAScript 6》
https://www.gitbook.com/book/oshotokill/understandinges6-simplified-chinese/details

你可能感兴趣的:(ES6)