[JS]带你掌握Iterator迭代器

959 阅读1分钟

前言

【Iterator】(迭代器)是ES6中JavaScript的一个新特性,拥有迭代器特性的数据,js可以为它执行如for...of,map(),filter()等迭代方法。掌握这个技巧之后,在某些开发业务常见中,我们可以利用迭代器做出更合适的技术选择。

迭代协议

值得注意的是,在ES6中我们看到围绕着迭代器,新增出了许多的API。如for...of,map(),filter()等。可是这些方法的实现,并不是ES在底层新增了一些内置实现或者语法。ES做的只是定制了一套【迭代协议】。

这个迭代协议有2个部分,【可迭代协议】和【迭代器协议】。

迭代器协议

迭代器协议规定了,一个迭代器应该要长什么样子。在官方规定中,一个对象想要被称为“迭代器”,就必须拥有next方法。而这个next方法调用之后会返回一个对象。这个对象有2个属性value和done。

  • value 是本次迭代的返回值。在done为true之后,value就没有意义了,但最好返回undefined。
  • done 是迭代结束的标识。

根据这个规范,我们大致可以写出一个迭代器应该是以下的样子:

var myIterator = {
  next:function(){
    count++;
    if(count<3){
      return {value: count,done:false};
    }else{
      return {value: undefined,done:true};
    }	 
	},
  count:0
}

可迭代协议

可是光有迭代器是没用的,我们需要有一个载体能让迭代器跑起来。这就是【可迭代协议】的工作了。可迭代协议规范了哪些数据可以作为运行迭代器的载体。在JavaScript中,String、Array、TypedArray、Map 和 Set 等都是内置的符合可迭代协议的类型。

其中最常见的调用迭代的方式之一就是for...of。

for(var i of [1,2,3]){
	console.log(i);
}

for(var i of "123"){
	console.log(i);
}

for(var i of new Set([1,2,3])){
	console.log(i);
}
// 以上均输出
// 1
// 2
// 3

Symbol.iterator

ok,现在我们知道了平常的一些类型可以调用迭代方法是因为他们符合了可迭代协议。可是这个协议具体是什么?我们可以手动的让其他类型也符合这个协议吗?

答案其实是 @@iterator 方法,只有一个数据身上有 @@iterator 方法就符合可迭代器协议了。而这个方法可以通过被【Symbol.iterator】属性引用。

为此我们可以在浏览器控制台输出array,map,set等类型,可以看到他们都内置了一个Symbol.iterator属性。

因此我们可以简单地理解为只有一个数据(不管是什么内类),需要它身上有【Symbol.iterator】属性,且这个属性指向一个符合迭代器协议的迭代器之后。这个数据就符合了可迭代协议。

手动实现Iterator

读到这里,其实我们已经做好了所有前置准备了。现在我们开始来手动为一个对象(非内置符合可迭代器协议的数据类型)实现for of 方法。

// 在js中对象类型,默认并不符合可迭代协议
var obj = {a:'a',b:'b'};
for(var i of obj){
	console.log(i);
}
// 执行会报错  Uncaught TypeError: obj is not iterable

我们先写一个迭起器。

// 根据迭代器协议,写一个迭代器
var myIterator = function(){
  var done = false;
  return {
    next: function(){
      if(done){
      	return {value:'hi',done:true};
      }else{
        done = true;
      	return {value:'going',done:false};
      }
    }
  }
}

然后给一个对象添加【Symbol.iterator】属性,并指向上面的写的迭代器。

var obj={};
obj[Symbol.iterator] = myIterator;

此时再调用for...of

for(var i of obj){
	console.log(i);
}
// 正常输出
// 'going'

可以看到,obj已经可以像内置符合迭代器的类型一样执行for...of了。

结合生成器优化写法

相信细心的朋友在看到next方法时,就想到了生成器。既然迭代是在不停地调用next方法,生成器函数又天生自带next方法,那么是不是可以直接用生成器来定义迭代器呢?答案是可以的,结合生成器我们可以写出更加简练的代码。

var myIterator = function*(){
  	yield 11;
  	yield 12;
  	yield 13;
  	yield 14;
  	yield 15;
}

// 定义一个空的数组
var arr = [];
arr[Symbol.iterator] = myIterator;
for(var i of arr){
	console.log(i);
}
// 输出
// 11
// 12
// 13
// 14
// 15

所以生成器究竟是对象还是函数?

通过上述的简化实现之后,我们发现结合生成器确实能实现我们想要的效果。可是本文前面不是说了,迭代器必须符合迭代器协议吗?迭代器不是应该是一个对象吗?为什么这里生成器是一个函数也符合这个协议?

其实大家不用怀疑,因为生成器他既是一个函数也是一个对象。

// 定义一个生成器
let aGeneratorObject = function* (){
    yield 1;
    yield 2;
    yield 3;
}();

typeof aGeneratorObject.next;
// 返回"function", 他有一个next方法,说明他符合迭代器协议

typeof aGeneratorObject[Symbol.iterator];
// 返回"function", 有一个@@iterator方法,说明生成器本身也是可迭代对象

aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// 返回true, 因为@@iterator方法返回自身(即迭代器),说明这是一个格式良好的可迭代对象

[...aGeneratorObject];
// 返回[1, 2, 3]

console.log(Symbol.iterator in aGeneratorObject)
// 返回true, 因为@@iterator方法是aGeneratorObject的一个属性

总结

今天我们掌握了JavaScript中的迭代器协议,知道了迭代器与可迭代对象的关系。并手动实现了自定义迭代器的效果。希望可以帮助大家对迭代器有深入的了解,并在开发中运用起来。

参考

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…