我们在用循环语句迭代数据时,必须初始化一个变量来记录每一次在数据集合的位置以及终止循环。为了简化数据集合的操作,ES6添加了迭代器Iterator及Generator特性来高效的处理数据。在for-of循环、扩展运算符(...)以及异步编程中都可以使用迭代器。
Iterator
迭代器是一种特殊对象,它具有专门为迭代过程设计的专有接口。所有的迭代器对象都有一个next()方法,每次调用都会返回一个结果对象,结果对象有两个属性:一个是value,表示将要返回的值;另一个是done,它是一个布尔类型的值,表示是否有可返回的数据。当没有更多可返回的值时value为undefined,done为true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每次调用next()方法都会返回下一个可用的值。
ES5语法创建一个迭代器对象
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
value: value,
done: done
}
}
}
}
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 }
由此可见ES5语法创建一个迭代器还是很复杂的,但ES6同时还引入了Generator特性,它可以让创建迭代器对象的过程变得更加简单。
Generator
生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中使用新的关键字yield指定将要被迭代的值。星号可以紧挨着function关键字,也可以在中间添加一个空格。
function* createIterator() {
yield 1;
yield 2;
yield 3;
}
const iterator = createIterator();
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 }
上述例子中,createIterator()前的星号表明它是一个生成器函数;yield关键字也是ES6的新特性,可以通过它来指定调用迭代器的next()方法时的返回值和返回顺序,生成器函数中的yield关键字可以暂停函数的执行。当引擎遇到yield关键字的时候,生成器的执行将被暂停,在下次运行该函数时,它会记住先前暂停的位置,然后从那里开始运行。
yield的使用有限制,它只能在生成器内部使用,在其他地方使用会导致程序抛出语法错误,即便在生成器内部的函数里使用也是如此。
function* createIterator(items) {
items.forEach(function(item) {
yield item + 1; // 抛出语法错误
})
}
Generator比较常见的应用场景就是生成状态机:
// 1.有限的状态机,可应用于状态不可逆场景,记录当前的调用状态
function* state() {
yield 'A';
yield 'B';
yield 'C'
}
const 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); // undifined
// 2.无限的状态机,应用于状态可逆的场景
function* state() {
while(true) {
yield 'A';
yield 'B';
yield 'C';
}
}
const 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
另外,我们也可以利用生成器来实现斐波那契数列:
function* fibonacci() {
let fn1 = 1;
let fn2 = 1;
while(true) {
let current = fn1;
fn1 = fn2;
fn2 = current + fn1;
yield current;
}
}
const sequence = fibonacci();
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next().value); // 13
console.log(sequence.next().value); // 21
...
可迭代对象
可迭代对象是具有Symbol.iterator属性,与迭代器密切相关的对象。Symbol.iterator通过制定的函数可以返回一个作用于附属对象的迭代器。在ES6中,所有的集合对象(Array,Set,Map)和字符串都是可迭代对象,这些对象中都有默认的迭代器。ES6新增的for-of循环需要用到可迭代对象的这些默认迭代器。
一个对象要实现可迭代,就必须实现@@iterator方法,即意味着这个对象(或其原型链中的任意对象)必须具有一个带有Symbol.iterator键的属性。自定义可迭代对象:
const myIterator = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}
for(let value of myIterator) {
console.log(value);
}
// 1
// 2
// 3
// 或者
[...myIterator] = [1,2,3]
在ES6中的可迭代对象都提供了内建迭代器。集合对象内置了三种迭代器,entries()、keys()和values(),之前已回顾过这里不再赘述,主要介绍一下字符串迭代器。
字符串迭代器
javascript中的字符串逐渐变得更像数组了(具有length属性和可迭代),ES6的目标是全面支持Unicode,因此通过for-of循环可以直接操作字符而不是编码单元。
// for-of可直接操作双字节字符
const message = 'A 吉 B';
for(let m of message) {
console.log(m);
}
// A
// (空)
// 吉
// (空)
// B
高级迭代器功能
迭代器的基础功能可以帮助我们完成很多任务,通过生成器创建迭代器的过程也很便捷,除了简单的集合遍历外,迭代器也可以用于完成一些复杂的任务,如异步编程等。
给迭代器传递参数
迭代器可以通过调用next()方法来返回值,而生成器可以通过yield关键字来生成值。若在调用迭代器的next()方法时传递参数,则这个参数的值会替代生成器内部上一条yield语句的返回值。
function* createIterator() {
let first = yield 1;
let second = yield first + 2;
yield second + 3;
}
const iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next(3).value); // 5
console.log(iterator.next(6).value); // 9
需要注意一点的是,第一次调用next()方法无论传递什么参数都会被丢弃。因为next()传递的参数会替代上一次yield的返回值,但第一次调用next()前不会执行任何yield语句。另外,=号左侧的赋值是在下一次调用next()方法时执行的。
在迭代器中抛出错误
除了给迭代器传递数据外,还可以给它传递错误条件。通过throw()方法,当迭代器恢复执行时可令其抛出错误。这种主动抛出错误的能力对于异步编程至关重要,也提供了模拟结束函数的两种方法(返回值或抛出错误)。
function* createIterator() {
const first = yield 1;
const second = yield first + 2;
yield second + 3;
}
const iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next(3).value); // 5
console.log(iterator.throw(new Error("Boom"))); // 从生成器内部抛出错误
console.log(iterator.next()); // 不会被执行
迭代器在调用throw()方法,在继续执行second赋值时,错误就从生成器内部抛出并阻止了代码继续执行,此后的代码被中止执行了。至此,我们便可在生成器内部通过try-catch捕获这些错误。
function* createIterator() {
const first = yield 1;
const second;
try {
second = yield first + 2;
} catch (e) {
second = 6;
}
yield second + 3;
}
const iterator = createIterator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(3)); // { value: 5, done: false }
console.log(iterator.throw(new Error("Boom"))); // { value: 9, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
通过try-catch包裹第二条yield语句,同样在迭代器调用throw()方法后,在try代码块中执行second赋值时还是会主动抛出错误,但catch代码块捕获错误后将second赋值为6,然后继续执行下一条yield语句返回9。
可以看到调用throw()方法也会像调用next()方法一样,返回一个结果对象。这是因为生成器内部捕获了这个错误并继续执行了后面的yield语句。如此看来,next()和throw()就像是迭代器的两条指令,调用next()命令迭代器继续执行,调用throw()也会命令迭代器继续执行,并同时抛出一个错误。
生成器返回语句
生成器也是函数,因此可通过return语句提前退出函数执行,并且可以指定一个返回值。在生成器中,return表示所有操作已经完成,属性done被设置为true;若同时提供了相应的值,则属性value会被设置为这个值。
function* createIterator() {
yield 1;
return;
yield 2;
yield 3;
}
const iterator = createIterator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: undedined, done: true }
console.log(iterator.next()); // { value: undefined, done: true }
iterator第一次调用next()方法时,生成器内部执行第一条yield语句以及后面的return语句,之后的yield语句将不会被执行。return语句也可以指定返回值并赋值给返回对象的value属性。
function* createIterator() {
yield 1;
return 10;
yield 2;
yield 3;
}
const iterator = createIterator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 10, done: true }
console.log(iterator.next()); // { value: undefined, done: true }
可以看到,return语句指定的返回值,只会在返回对象中出现一次,后续继续调用next()方法后value属性被重置为undefined。
委托生成器
某些情况下,若想将两个迭代器合二为一,这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他迭代器。
function* createIterator1() {
yield 1;
yield 2;
}
function* createIterator2() {
yield 'red';
yield 'green';
}
function* createCombinedIterator() {
yield *createIterator1();
yield *createIterator2();
yield true;
}
const 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: false }
生成器createCombinedIterator先后委托了两个生成器createIterator1和createIterator2,每次调用next()方法就会委托相应的迭代器返回相应的值,直到无法返回更多的值就会执行最后一条yield语句并返回true。
当然,可以利用生成器委托的这个功能,处理一些复杂的任务,如:
function* createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function* createRepeatingItertor(count) {
for(let i = 0; i< count; i++) {
yield 'repeat';
}
}
function* createCombinedIterator() {
const result = yield *createNumberIterator();
yield *createRepeatingIiterator(result);
}
const 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: false }
在生成器createCombinedIterator中,执行过程先被委托给生成器createNumberIterator,返回值在迭代器第三次调用next()方法时赋值给result,并将其传递给生成器createRepeatingIiterator执行它的第一条yield语句,因为count为3,因此createRepeatingIiterator会被执行三次。
事实上生成器的特性多与异步编程相关,在生成器和迭代器的所有应用场景中,最有趣的可能就是用来创建更简洁的异步代码。笔者后续也会继续补充这一块的内容。
【参考文献】:[1]【美】Nicholas C. Zakas.深入理解ES6[M].电子工业出版社,2017.7