ES6 之 Iterator和Generator

233 阅读7分钟

1. for...of 循环

1. 现有的各种循环存在的问题

  • for,写法上比较繁琐
  • forEach,无法使用 break 或 return 跳出循环;和其他循环不同,它是一个函数
  • for...in,不仅遍历数字键名,还会遍历其他键;遍历出的键是字符串,不是数字类型。for...in 更适合遍历对象。

所以为了解决这些问题,ES6新增了一个for...of循环,写法简洁,又没有其他循环的缺点。

let arr = ['red', 'green', 'blue', null];
arr.color = 'default';
for (var i=0; i<arr.length; i++) {
  console.log(arr[i]);  //'red' 'green' 'blue' null
}
arr.forEach((value, key) => {
  // if (value === null) break; //SyntaxError: Illegal break statement
  console.log(key, value); 
  //0 'red'
  //1 'green'
  //2 'blue'
  //3  null
})
for (var index in arr) {
  console.log(index); //'0' '1' '2' '3' 'color'
}
for (var value of arr) {
  if (value === null) break;
  console.log(value)  //'red' 'green' 'blue'
}

2. 使用 for...of

想使用for...of 循环遍历数据,需要这个数据结构满足一个特点,就是它需要具备 Iterator 接口。 也就是说 for...of 循环是遍历所有具备 Iterator 接口数据结构的统一的方法。

数组SetMap 结构、argumentsDOM NodeList 对象、字符串、以及 Generator 对象。

以上数据结构原生具备 Iterator 接口,Object并不具备
这是因为 for...of 循环是一种线性循环,是按照一定的输出顺序遍历的
所以对象更适合的遍历方式是 for...in,如果想使用for...of ,就需要手动添加 Iterator 接口。

2. Iterator 遍历器

1. 概念

Iterator(遍历器)是一种接口机制,用来处理不同的数据结构。用来供for...of消费使用。
遍历器本质是一个指针对象,对象里有一个 next
方法,指针最开始指向数据的起始位置,每调用一次next方法,指针向后移动一个位置,并返回一个对象,包含valuedone两个属性。

{
  next: function () {
    if (/*条件*/) {
      return {value: 123, done: false}
    }
    return {value: undefined, done: true};
  }
}

2. 默认 Iterator 接口

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性。
只要一个数据结构定义了一个 Symbol.iterator 的属性,这个属性是一个函数,并且返回一个带有 next 方法的对象,那么这个数据就是可遍历的(iterable),可以被for...of遍历。

let arr = ['a', 'b', 'c'];
let it = arr[Symbol.iterator]();
console.log(it.next())  //{ value: 'a', done: false }
console.log(it.next())  //{ value: 'b', done: false }
console.log(it.next())  //{ value: 'c', done: false }
console.log(it.next())  //{ value: undefined, done: true }

3. 调用 Iterator 接口的场合

  • for...of
  • 解构赋值
  • 扩展运算符
let set = new Set().add('a').add('b').add('c');
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()
  • Promise.all(), Promise.race()
  • yield*

4. return(),throw()

遍历器除了可以定义next方法(必需),还可以定义returnthrow方法(非必需)。

3. Generator 生成器

1. 与普通函数的区别

在写法上有两点区别,一是在function关键字后面多了一个 *;二是函数内部有yield关键字。
yield 字段只能在 Generator 内部使用,它能使 Generator 函数“暂停”。
在使用上有很大的不同, Generator 函数调用后并不会马上执行,而是返回一个对象,每调用一次对象上的next方法,会产出一个数据。
Generator也可以理解成一个状态机,通过yield字段控制内部指针指向。

function* gen () {
  yield 'hello';
  yield 'world';
  return 'ending!';
}
let it = gen();
console.log(it.next())  //{ value: 'hello', done: false }
console.log(it.next())  //{ value: 'world', done: false }
console.log(it.next())  //{ value: 'ending', done: true }

2. 与 Iterator 的关系

可以看到上面调用生成器函数gen后返回的是一个带有next方法的对象,这个对象就是一个 Iterator。
Generator 函数就是遍历器生成函数,调用 Generator 函数会返回一个遍历器 (Iterator) 。
因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

//...
var myIterable = {};
myIterable[Symbol.iterator] = gen;
console.log([...myIterable]) //[ 'hello', 'world' ]

需要注意的是myIterable只有两个数据,并没有ending,因为它的done是true,同样for...of也不会遍历到ending。

3. next 参数

一般情况下,yield的返回值是undefined,如果next方法有参数,yield的返回值就是这个参数的值。

function* f(count) {
  let x = yield count;
  let y = x + (yield x);
  yield y + (x + (yield x + y));
  return 'ending';
}
var g = f(10);

console.log(g.next()) //{ value: 10, done: false }
console.log(g.next(1)) //{ value: 1, done: false }
console.log(g.next(2)) //{ value: 4, done: false }
console.log(g.next(3)) //{ value: 7, done: false }
console.log(g.next(4)) //{ value: 'ending', done: true }
  • 第一次调用next不能传参。
  • 第二次调用传了 1,并且赋值给x,然后产出了count为10
  • 第三次调用传了 2,并且赋值到运算公式(let y = x + (yield x) => let y = 1 + 2),然后产出了x为1
  • 依次调用下去,最后一次虽然传了值但是最后yield并没有赋值操作,也就没有用到。 从这个例子中可以看到几个特点
  • (1). Generator 函数遇到 yield 就会暂停产出一个结果。
  • (2). next传参了,yield就有值,反之则是undefined
  • (3). yield 参与运算只关心yield的值,并不关心yield后面产出的结果,假设上面第四次调用不传值,则会产出 NaN。

4. yield* 表达式

在一个 Generator 函数里调用另一个 Generator 函数,可以使用yield* 表达式。

function* gen1 () {
  yield 1;
  yield 2;
}
function* gen2 () {
  yield 3;
  yield* gen1();
  yield 4;
}
for (let value of gen2()) {
  console.log(value)  //3 1 2 4
}
  • yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。
function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

// 等同于
function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}
  • 有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。
  • yield* 后面可以执行任何一个带有 Iterator 接口的数据
function* gen () {
  yield* new Set([3, 2, 1]);
}
console.log([...gen()]) //[ 3, 2, 1 ]

5. return(),throw()

自定义的遍历器必须包含next()方法,return()和throw()是可以不写的。 Generator函数生成的遍历器默认这3种方法都会有。

  • return()方法可以返回给定的值,并且终结遍历 Generator 函数。遇到 try{}finally{}代码块会立即执行finally里面的逻辑,最后返回return的值。
function* gen () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
  return 7;
}
let it = gen();
console.log(it.next()); //{ value: 1, done: false }
console.log(it.next()); //{ value: 2, done: false }
console.log(it.return(8));  //{ value: 4, done: false }
console.log(it.next()); //{ value: 5, done: false }
console.log(it.next()); //{ value: 8, done: true }
  • throw()方法可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
function* gen() {
  try {
    yield console.log(1);
    yield console.log(2);
    yield console.log(3);
    yield console.log(4);
  } catch (e){
    console.log('内部捕获', e)
  }
  yield console.log(5);
  yield console.log(6);
  yield console.log(7);
}

let it = gen();
try {
  throw new Error('777')
  it.throw('a');  //不会执行
} catch (e) {
  console.log('throw命令抛出外部捕获',e)   //捕获到 777
}
try {
  it.next();  // 1
  it.next();  // 2
  it.throw('b');  // 内部捕获 b,并执行 yield 打印 5
  it.next();  // 6
  it.throw('c');  // 外部捕获 c
  it.next();  // 不会执行了
  it.next();  // 不会执行了
} catch(e) {
  console.log('外部捕获', e)
}
// throw命令抛出外部捕获 Error: 777 ...
// 1
// 2
// 内部捕获 b
// 5
// 6
// 外部捕获 c

(1). throw命令抛出的错误只能在外部捕获,并且try代码块里的剩余代码不会再执行。
(2). throw()方法执行前至少执行前至少要执行一次next()方法。
(3). throw()方法抛出的错误想在函数内部捕获需要 Generator 函数内部用 try catch 包裹。
(4). throw()方法被内部捕获后 try 代码块剩余代码还可以继续执行。并且会相当于执行一次next
(5). throw()方法被外部捕获后,相当于用 throw 命令抛出错误,会终止 try 代码块内的代码。

next()、throw()、return()这三个方法本质上是同一件事,它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

参考 es6.ruanyifeng.com/