JavaScript < ES5、ES6、ES7、… >ECMAScript 6

ES6(十八):迭代器(Iterator)和生成器(Genera

2019-02-11  本文已影响61人  CodeMT

前面的话


  用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素

迭代器的使用可以极大地简化数据操作,于是ES6也向JS中添加了这个迭代器特性。新的数组方法和新的集合类型(如Set集合与Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的for-of循环、展开运算符(...),甚至连异步编程都可以使用迭代器

本文将详细介绍ES6中的迭代器(Iterator)和生成器(Generator)

引入

下面是一段标准的for循环代码,通过变量i来跟踪colors数组的索引,循环每次执行时,如果i小于数组长度len则加1,并执行下一次循环

var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
  console.log(colors[i]);
}

迭代器

迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value,表示下一个将要返回的值;另一个是done,它是一个布尔类型的值,当没有更多可返回数据时返回true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次next()方法,都会返回下一个可用的值

如果在最后一个值返回后再调用next()方法,那么返回的对象中属性done的值为true,属性value则包含迭代器最终返回的值,这个返回值不是数据集的一部分,它与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回undefined

下面用ES5的语法创建一个迭代器

function createIterator(items) {
  var i = 0;    return {
    next: function() {            
      var done = (i >= items.length);
      var value = !done ? items[i++] : undefined;            
      return {
        done: done,
        value: value
      };
    }
  };
}
var iterator = createIterator([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 }"// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

生成器

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

// 生成器
function *createIterator() {
  yield 1;
  yield 2;
  yield 3;
}
// 生成器能像正规函数那样被调用,但会返回一个迭代器
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
function *createIterator(items) {    
for (let i = 0; i < items.length; i++) {
  yield items[i];
  }
}
let iterator = createIterator([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 }"// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

【使用限制】

yield关键字只可在生成器内部使用,在其他地方使用会导致程序抛出错误

function *createIterator(items) {
  items.forEach(function(item) {       
    // 语法错误
    yield item + 1;
  });
}

【生成器函数表达式】

也可以通过函数表达式来创建生成器,只需在function关键字和小括号中间添加一个星号(*)即可

let createIterator = function *(items) {    
for (let i = 0; i < items.length; i++) {
  yield items[i];
  }
};
let iterator = createIterator([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 }"// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"

[注意]不能用箭头函数来创建生成器

【生成器对象的方法】

由于生成器本身就是函数,因而可以将它们添加到对象中。例如,在ES5风格的对象字面量中,可以通过函数表达式来创建生成器

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

【状态机】

生成器的一个常用功能是生成状态机

let state = function*(){    
  while(1){
    yield 'A';
    yield 'B';
    yield 'C';
  }
}
let status = state();
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
console.log(status.next().value);//'C'
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'

可迭代对象

可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象。Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。在ES6中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。ES6中新加入的特性for-of循环需要用到可迭代对象的这些功能

[注意]由于生成器默认会为Symbol.iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象

let values = [1, 2, 3];
for (let num of values) {
  //1
  //2
  //3 
  console.log(num);
}

[注意]如果将for-of语句用于不可迭代对象、nullundefined将会导致程序抛出错误

【访问默认迭代器】

可以通过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 isIterable(object) {
  return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(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 x of collection) {
  //1
  //2
  //3 
  console.log(x);
}

【展开运算符和非数组可迭代对象】

通过展开运算符(...)可以把Set集合转换成一个数组

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
let map = new Map([ ["name", "huochai"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "huochai"], ["age", 25]]
let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102],
allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]

内建迭代器

迭代器是ES6的一个重要组成部分,在ES6中,已经默认为许多内建类型提供了内建迭代器,只有当这些内建迭代器无法实现目标时才需要自己创建。通常来说当定义自己的对象和类时才会遇到这种情况,否则,完全可以依靠内建的迭代器完成工作,而最常使用的可能是集合的那些迭代器

【集合对象迭代器】

ES6中有3种类型的集合对象:数组、Map集合与Set集合

为了更好地访问对象中的内容,这3种对象都内建了以下三种迭代器

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

entries()迭代器

每次调用next()方法时,entries()迭代器都会返回一个数组,数组中的两个元素分别表示集合中每个元素的键与值。如果被遍历的对象是数组,则第一个元素是数字类型的索引;如果是Set集合,则第一个元素与第二个元素都是值(Set集合中的值被同时作为键与值使用);如果是Map集合,则第一个元素为键名

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let entry of colors.entries()) {
  console.log(entry);
}
for (let entry of tracking.entries()) {
  console.log(entry);
}
for (let entry of data.entries()) {
  console.log(entry);
}
[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ES6"]
["format", "ebook"]

values()迭代器

调用values()迭代器时会返回集合中所存的所有值

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let value of colors.values()) {
  console.log(value);
}
for (let value of tracking.values()) {
  console.log(value);
}
for (let value of data.values()) {
  console.log(value);
}
"red"
"green"
"blue"
1234
5678
9012
"Understanding ES6"
"ebook"

keys()迭代器

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let key of colors.keys()) {
  console.log(key);
}
for (let key of tracking.keys()) {
  console.log(key);
}
for (let key of data.keys()) {
  console.log(key);
}
0
1
2
1234
5678
9012
"title"
"format"

不同集合类型的默认迭代器

每个集合类型都有一个默认的迭代器,在for-of循环中,如果没有显式指定则使用默认的迭代器。数组和Set集合的默认迭代器是values()方法,Map集合的默认迭代器是entries()方法。有了这些默认的迭代器,可以更轻松地在for-of循环中使用集合对象

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "print");
// 与使用 colors.values() 相同
for (let value of colors) {
  console.log(value);
}
// 与使用 tracking.values() 相同
for (let num of tracking) {
  console.log(num);
}
// 与使用 data.entries() 相同
for (let entry of data) {
  console.log(entry);
}
"red"
"green"
"blue"
1234
5678
9012["title", "Understanding ES6"]
["format", "print"]

【字符串迭代器】

ES5发布以后,JS字符串慢慢变得更像数组了,例如,ES5正式规定可以通过方括号访问字符串中的字符(也就是说,text[0]可以获取字符串text的第一个字符,并以此类推)。由于方括号操作的是编码单元而非字符,因此无法正确访问双字节字符

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

B
var message = "A 𠮷 B" ;
for (let c of message) {
  console.log(c);
}
A

𠮷

B

【NodeList迭代器】

DOM标准中有一个NodeList类型,document对象中的所有元素都用这个类型来表示。对于编写Web浏览器环境中的JS开发者来说,需要花点儿功夫去理解NodeList对象和数组之间的差异。二者都使用length属性来表示集合中元素的数量,都可以通过方括号来访问集合中的独立元素。而在内部实现中,二者的表现非常不一致,因而会造成很多困扰

var divs = document.getElementsByTagName("div");
for (let div of divs) {
  console.log(div.id);
}

高级迭代器

迭代器的基础功能可以辅助完成很多任务,通过生成器创建迭代器的过程也很便捷,除了这些简单的集合遍历任务之外,迭代器也可以被用于完成一些复杂的任务

【给迭代器传递参数】

迭代器既可以用迭代器的next()方法返回值,也可以在生成器内部使用yield关键字来生成值。如果给迭代器的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 }"

【在迭代器中抛出错误】

除了给迭代器传递数据外,还可以给它传递错误条件。通过throw()方法,当迭代器恢复执行时可令其抛出一个错误。这种主动抛出错误的能力对于异步编程而言至关重要,也能提供模拟结束函数执行的两种方法(返回值或抛出错误),从而增强生成器内部的编程弹性。将错误对象传给throw()方法后,在迭代器继续执行时其会被抛出

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2; // yield 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"))); // 从生成器中抛出了错误
function *createIterator() {
  let first = yield 1;
  let second;    try {
    second = yield first + 2; // yield 4 + 2 ,然后抛出错误
  } 
  catch (ex) {
    second = 6; // 当出错时,给变量另外赋值 
  }
  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"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

【生成器返回语句】

由于生成器也是函数,因此可以通过return语句提前退出函数执行,对于最后一次next()方法调用,可以主动为其指定一个返回值。正如在其他函数中那样,可以通过return语句指定一个返回值。而在生成器中,return表示所有操作已经完成,属性done被设置为true;如果同时提供了相应的值,则属性value会被设置为这个值

function *createIterator() {
  yield 1;    
  return;
  yield 2;
  yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
function *createIterator() {
  yield 1;    
  return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

[注意]展开运算符与for-of循环语句会直接忽略通过return语句指定的任何返回值,只要done一变为true就立即停止读取其他的值。不管怎样,迭代器的返回值依然是一个非常有用的特性

【委托生成器】

在某些情况下,我们需要将两个迭代器合二为一,这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他生成器。当定义这些生成器时,只需将星号放置在关键字yield和生成器的函数名之间即可

function *createNumberIterator() {
  yield 1;
  yield 2;
}
function *createColorIterator() {
  yield "red";
  yield "green";
}
function *createCombinedIterator() {
  yield *createNumberIterator();
  yield *createColorIterator();
  yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
function *createNumberIterator() {
  yield 1;
  yield 2;    
  return 3;
}
function *createRepeatingIterator(count) {    
  for (let i=0; i < count; i++) {
    yield "repeat";
  }
}
function *createCombinedIterator() {
  let result = yield *createNumberIterator();
  yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
function *createNumberIterator() {
  yield 1;
  yield 2;    
  return 3;
}
function *createRepeatingIterator(count) {    
  for (let i=0; i < count; i++) {
    yield "repeat";
  }
}
function *createCombinedIterator() {
  let result = yield *createNumberIterator();
  yield result;
  yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
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: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

[注意]yield*也可直接应用于字符串,例如yield* "hello",此时将使用字符串的默认迭代器

异步任务执行

生成器令人兴奋的特性多与异步编程有关,JS中的异步编程有利有弊:简单任务的异步化非常容易;而复杂任务的异步化会带来很多管理代码的挑战。由于生成器支持在函数中暂停代码执行,因而可以深入挖掘异步处理的更多用法

let fs = require("fs");
fs.readFile("config.json", function(err, contents) {    
  if (err) {
    throw err;
  }
  doSomethingWith(contents);
  console.log("Done");
});

【简单任务执行器】

由于执行yield语句会暂停当前函数的执行过程并等待下一次调用next()方法,因此可以创建一个函数,在函数中调用生成器生成相应的迭代器,从而在不用回调函数的基础上实现异步调用next()方法

function run(taskDef) {
  // 创建迭代器,让它在别处可用
  let task = taskDef();// 启动任务
  let result = task.next();// 递归使用函数来保持对 next() 的调用
  function step() {
    // 如果还有更多要做的
    if (!result.done) {
      result = task.next();
      step();
    }
  }
  // 开始处理过程 step();
}
run(function*() {
  console.log(1);
  yield;
  console.log(2);
  yield;
  console.log(3);
});

【向任务执行器传递数据】

给任务执行器传递数据的最简单办法是,将值通过迭代器的next()方法传入作为yield的生成值供下次调用。在这段代码中,只需将result.value传入next()方法即可

function run(taskDef) {// 创建迭代器,让它在别处可用
  let task = taskDef();// 启动任务
  let result = task.next();// 递归使用函数来保持对 next() 的调用
  function step() {
  // 如果还有更多要做的
    if (!result.done) {
      result = task.next(result.value);
        step();
      }
    }
    // 开始处理过程 step();
}
run(function*() {
  let value = yield 1;
  console.log(value); // 1
  value = yield value + 3;
  console.log(value); // 4
});

【异步任务执行器】

之前的示例只是在多个yield调用间来回传递静态数据,而等待一个异步过程有些不同。任务执行器需要知晓回调函数是什么以及如何使用它。由于yield表达式会将值返回给任务执行器,所有的函数调用都会返回一个值,因而在某种程度上这也是一个异步操作,任务执行器会一直等待直到操作完成

function fetchData() {
  return function(callback) {
    callback(null, "Hi!");
  };
}
function fetchData() {
  return function(callback) {
    setTimeout(function() {
      callback(null, "Hi!");
    }, 50);
  };
}
function run(taskDef) {
  // 创建迭代器,让它在别处可用
  let task = taskDef();// 启动任务
  let result = task.next();// 递归使用函数来保持对 next() 的调用
  function step() {
    // 如果还有更多要做的
    if (!result.done) {            
      if (typeof result.value === "function") {
        result.value(function(err, data) {                    
          if (err) {
            result = task.throw(err);                        
            return;
          }
          result = task.next(data);
          step();
        });
      } else {
        result = task.next(result.value);
        step();
      }
    }
  }
  // 开始处理过程 step();
}
let fs = require("fs");    
function readFile(filename) {
  return function(callback) {
    fs.readFile(filename, callback);
  };
}
run(function*() {
  let contents = yield readFile("config.json");
  doSomethingWith(contents);
  console.log("Done");
});
上一篇 下一篇

猜你喜欢

热点阅读