迭代器(Iterator)和生成器(Generator)之一

用循环语句迭代数据时,需要初始化一个变量来记录每一次迭代在数据集合中的位置,使用迭代器对象返回迭代过程中集合每一个元素,可以极大简化数据操作。

迭代器

迭代器是一种特殊的对象,他有一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性,一个是value,表示下一个将要返回的值,一个是done,他是一个布尔类型的值,当没有更多可返回的数据时返回true·。迭代器还会保存一个内部指针,用来指向当前集合中的位置,每调用一次next()方法,都会返回下一个可用的值。

如果在最后一个值返回后再调用next()方法,返回对象中属性done的值为true,属性value的值为包含迭代器最终返回的值,这个值不是数据集的一部分,如果没有相关数据则返回undefined

下面用ES5来模拟一个迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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([2,4,6]);
console.log(iterator.next()); //{ done: false, value: 2 }
console.log(iterator.next()); //{ done: false, value: 4 }
console.log(iterator.next()); //{ done: false, value: 6 }
console.log(iterator.next()); //{ done: true, value: undefined }

生成器

生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,星号可以紧挨着function关键字,也可以在中间添加一个空格。函数中用到关键词yield,可以通过它来指定调用迭代器的next()方法时的返回值及返回顺序。

1
2
3
4
5
6
7
8
9
function *createIterator(){
yield 1;
yield 2;
}
//生成器的调用和其他函数一样,最终返回的是创建好的迭代器
let iterator = createIterator();
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next()); //{ value: 2, done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

每当执行完一条yield语句后,函数就会自动停止执行。每次调用next()方法,函数会继续执行并执行下一条yield语句。上面代码中,执行完yield 1语句之后,函数便不在执行其他语句,直到再次调用迭代器的next()方法才会继续执行yield 2语句。使用yield关键字可以返回任何值或表达式,所以可通过生成器函数批量地给迭代器添加元素。

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

yield关键字只可在生成器内部使用,在其他地方使用会导致程序抛出语法错误,即使在生成器内部的函数里使用也是如此。比如上例生成器内部改为items.forEach(i => {yield i});会抛出错误。

  • 生成器表达式
    可以通过函数表达式来创建生成器,在function关键字和小括号中间添加一个星号(*)即可
    1
    2
    3
    4
    5
    let createIterator = function *(items){
    for(let i = 0; i < items.length; i++){
    yield items[i]
    }
    }

不能用箭头函数来创建生成器

  • 生成器对象的方法
    由于生成器本身是就是函数,可以将它们添加到对象中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let o = {
    createIterator: function *(items){
    for(let i = 0; i < items.length; i++){
    yield items[i]
    }
    }
    //ES6函数方法的简写方式
    // *createIterator(items){
    //...
    // }
    }
    let iterator = o.createIterator([2,4,5]);

可迭代对象和for...of循环

可迭代对象具有Symbol.iterator属性,该属性是一个生成器函数。Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。ES6中,所有集合对象(数组、TypedArraySet集合及Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。

生成器默认会为Symbol.iterator属性赋值,通过生成器创建的迭代器都是可迭代对象。

for...of循环需要用到可迭代对象的功能,循环每执行一次都会调用可迭代对象的next()方法,并将迭代器返回结果对象的value属性存储在一个变量中,循环遇到返回结果对象done属性的值为undefined时终止。

1
2
3
4
let arr = [2,4,6];
for (let num of arr){
console.log(num);
}

for...of循环通过调用数组arrSymbol.iterator方法来获取迭代器,这一过程是在JavaScript引擎中完成的。随后迭代器的next()方法别多次调用,从其返回对象的value属性读取值并存储在变量num中,当结果对象done属性值为true时循环退出,所以num不会被赋值undefined

for...of语句用于不可迭代对象、nullundefined将会导致程序抛出错误。

  • 访问默认迭代器

可以通过Symbol.iterator来访问对象默认的迭代器

1
2
3
4
5
6
let arr = [2,4,6];
let iterator = arr[Symbol.iterator]();
console.log(iterator.next()); //{ value: 2, done: false }
console.log(iterator.next()); //{ value: 4, done: false }
console.log(iterator.next()); //{ value: 6, done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

具有Symbol.iterator属性的对象都有默认的迭代器,可以用它来检测对象是否为可迭代对象。

1
2
3
function isIterable(obj){
return typeof obj[Symbol.iterator] === 'function'
}

  • 创建可迭代对象

默认情况下,开发者定义的对象是不可迭代对象,可以给Symbol.iterator属性添加一个生成器,这样就可以将其变成可迭代对象。

1
2
3
4
5
6
7
8
9
10
11
12
let collection = {
items: [2,4,6],
*[Symbol.iterator]() {
for(let item of this.items){
yield item
}
}
}
for (const x of collection) {
console.log(x);
}
//2,4,6

先创建一个生成器(星号仍然在属性名前)并将其赋值给对象的Symbol.iterator属性来创建默认的迭代器。生成器中通过for-of循环迭代this.items并用yield返回每一个值。

内建迭代器

ES6中,已经为许多内建类型提供了内建迭代器。

集合对象迭代器

ES6中有3种类型的集合对象:数组、Map集合与Set集合。这3种对象都内建了以下3种迭代器

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

entries()

每次调用next()方法时,entries()迭代器都会返回一个数组,数组的两个元素分别表示集合中的每个元素的键与值。如果是Set集合,第一个元素和第二个元素都是值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let colors = ['blue', 'red'];
let tracking = new Set([23,45]);
let data = new Map();
data.set('title', 'understanding');
data.set('format', 'kk')
for( let entry of colors.entries() ){
console.log(entry);
}
//[0, 'blue']
//[1, 'red']
for( let entry of tracking.entries() ){
console.log(entry);
}
//[23, 23]
//[45, 45]
for( let entry of data.entries() ){
console.log(entry);
}
//['title', 'understanding']
//['format', 'kk']

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for( let entry of colors.values() ){
console.log(entry);
}
//'blue'
//'red'
for( let entry of tracking.values() ){
console.log(entry);
}
//23
//45
for( let entry of data.values() ){
console.log(entry);
}
//'understanding'
//'kk'

keys()

keys()返回集合中存在的每一个键,如果是Set集合,由于键和值是相同的,所以keys()values()返回的也是相同的迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for( let entry of colors.keys() ){
console.log(entry);
}
//0
//1
for( let entry of tracking.keys() ){
console.log(entry);
}
//23
//45
for( let entry of data.keys() ){
console.log(entry);
}
//'title'
//'format'

默认迭代器
每个集合类型都有一个默认的迭代器,在for...of循环中,没有显示指定则使用默认迭代器。数组(包括TypedArray)和Set集合默认迭代器是values()方法,Map集合默认迭代器是entries()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for( let entry of colors ){
console.log(entry);
}
//'blue'
//'red'
for( let entry of tracking ){
console.log(entry);
}
//23
//45
for( let entry of data ){
console.log(entry);
}
//[ 'title', 'understanding' ]
//[ 'format', 'kk' ]

字符串迭代器

ES5规定,字符串可以通过方括号访问字符,由于方括号操作的是编码单元而不是字符,所以无法正确访问双字节字符。

1
2
3
4
5
let str = 'a𠮷b';
for (let i = 0; i< str.length; i++){
console.log(str[i]);
}
//a��b

可以通过字符串的默认迭代器来解决这个问题,其操作的是字符而不是编码单元。

1
2
3
4
for( let c of str ){
console.log(c);
}
//a𠮷b

NodeList迭代器

DOM标准中有一额NodeList类型,document对象中的所有元素都用这个类型来表示。ES6添加了默认迭代器后,NodeList也有默认迭代器表现与数组一致。

1
2
3
4
var divs = document.getElementsByTagName('div');
for (const div of divs) {
console.log(div.id);
}

展开运算符与非数组可迭代对象

展开运算符可以操作所有可迭代对象,根据默认迭代器来选取要引用的值,从迭代器读取所有值。

1
2
3
4
5
6
7
let set = new Set([3,5,6,7]),
arr1 = [...set];
let map = new Map([['name', 'nicholas'], ['age', 20]]),
arr2 = [...map];
console.log(arr1); //[ 3, 5, 6, 7 ]
console.log(arr2); //[ [ 'name', 'nicholas' ], [ 'age', 20 ] ]

将一个可迭代对象转成转化为数组,这是最简单的方法。可以将字符串中的每一个字符存入新数组,也可以将NodeList对象中的每一个节点存入新的数组中。