- 迭代模式
- ES6迭代器标准化接口
- 迭代循环
- 自定义迭代器
- 迭代器消耗
一、迭代模式
迭代模式中,通常有一个包含某种数据集合的对象。该数据可能存在一个复杂数据结构内部,而要提供一种简单的方法能够访问数据结构中每个元素。对象消费者并不需要知道如何组织数据,所有需要做的就是取出单个数据进行工作。
迭代模式API的设计:通常会设置一个next()方法来获取每个元素;为了方便操作,会设置一个hasNext()方法或者done属性来判断是否已经到达数据的末尾;
基于上面的简单设计思想,假设对象名为agg,可以在类似下面这两个示例一样迭代消费对象agg的数据:
1 //一: 2 var element; 3 while(element = agg.next()){ 4 console.log(element); 5 } 6 //二: 7 while(agg.hasNext()){//或者agg.done 8 console.log(agg.next()); 9 }
下面使用普通的数组作为agg的数据结构,来模拟实现一个迭代器:
1 var agg = (function(){ 2 var index = 0; 3 var data = [1,2,3,4,5,]; 4 var length = data.length; 5 return { 6 next:function(){ 7 var element; 8 if(!this.hasNext()){ 9 return null; 10 } 11 element = data[index]; 12 index ++; 13 return element; 14 }, 15 hasNext:function(){ 16 return index < length; 17 } 18 }; 19 }()); 20 while(agg.hasNext()){ 21 console.log(agg.next()); 22 }
为了实现迭代器多次迭代数据的能力,还可以提供额外的rewind()方法来实现重置迭代器;另外可能出现不前进指针返回当前元素的情况,可以设置current()方法来实现:
1 var agg = (function(){ 2 var index = 0; 3 var data = [1,2,3,4,5,]; 4 var length = data.length; 5 return { 6 next:function(){ //迭代数据 7 var element; 8 if(!this.hasNext()){ 9 return null; 10 } 11 element = data[index]; 12 index ++; 13 return element; 14 }, 15 hasNext:function(){ //判断迭代器是否到达数据的末尾 16 return index < length; 17 }, 18 rewind:function(){ //重置迭代器 19 index = 0; 20 }, 21 current:function(){ //不前进指针返回当前元素 22 return data[index]; 23 } 24 }; 25 }());
二、ES6迭代器标准化接口
~ES6迭代器next()接口(必须API):Iterator [required]
next(){method}:取得下一个IteratorResult
~ES6迭代器支持的扩展接口(可选API)成员:Iterator [optional]
return() {method}:停止迭代器并返回IteratorResult throw() {method}:报错并返回IteratorResult
~IteratorResult接口指令:IteratorResult
value {property}:当前迭代值或者最终返回值(如果undefined为可选的)
done {property}:布尔值,指示完成状态
~ES6迭代器还为必须提供生成器的需求,提供一个生成器对象的接口Iterable:
@@iterator() {method}:产生一个Iterator
@@iterator是一个特殊的内置符号,表示可以为这个对象产生迭代器的方法。
IteratorResult接口指定了从任何迭代器操作返回的值必须是下面这种形式的对象:
{ value: .. , done: true/false }
内置迭代器总是返回这种形式的值,如果有需求返回值还可以有更多属性;JavaScript不支持任何“接口”的概念,所以代码符合规范指示单纯的惯用法,但是JavaScript期望迭代器的指令(如for..of循环)所提供的东西必须符合这些接口,否在代码会失败。
三、迭代循环
在ES6中,新增了一个循环指令for..of来实现数据迭代,在ES6之前就有for、for..in、还有Array原型上的forEach方法都可以实现遍历,为什么还要新增for..of呢?for..of与迭代器又有什么关系呢?
for循环指令是根据自定义的条件来实现循环执行,并不一定用来遍历数据,这两种情况都不符合迭代模式,for只是一个简单的循环代码结构,但也可以用来遍历迭代数据。
for..in用来遍历可枚举对象属性,是基于对象属性特性实现的一个数据遍历的代码结构,返回数据名称,严格来说for..in是实现了枚举。因为根据对象属性枚举特性实现导致其可能不能完整迭代数据或者迭代不需要的数据(受属性特性限制),这也不符合迭代模式迭代返回数据有迭代接口实现的规范。
forEach是一个不完整的简单迭代器,其不能for..of一样使用break或者return fales一样中断迭代过程,也就是说缺少return接口。
1 var arr = [3, 5, 7]; 2 3 arr.forEach(function (value) { 4 console.log(value); 5 if (value == 5) { 6 return false; //这里并不能中断迭代器 7 } 8 }); 9 //3 5 7
但是for..of只能迭代带有迭代接口的数据,比如Array,Set,Map,Nodelist对象,因为这些数据类型原型都带有默认的迭代函数Symbol.iterator,所以对于普通的数据集可以根据ES6的迭代接口规范自定义迭代器来实现数据迭代。
如果阅读过我的上一篇博客ES6入门九:Symbol元编程会了解到Symbol对象上有一个静态属性iterator,这个属性就是用来实现自定义迭代接口的标识,被for..of执行迭代操作时会自动根据对象上的[Symbol.iterator]属性获取数据上的迭代接口,下面展示之前的自定义数据集及其迭代接口的实现方法:
1 var obj = { 2 0:"a", 3 1:"b", 4 2:"c", 5 length:3, 6 [Symbol.iterator]:function(){ 7 let curIndex = 0; 8 let next = () => { 9 return { 10 value:this[curIndex], 11 done:this.length == curIndex++, 12 } 13 } 14 return { 15 next 16 } 17 } 18 } 19 20 for(let p of obj){ 21 console.log(p); //输出a b c (如果不再obj上添加Symbol.iterator迭代方法,会报错:obj is not iterable) 22 }
四、自定义迭代器
在第三节的最后一个示例中演示了一个带有迭代器的对象obj,实现了ES6中的next()、IteratorResult的{value,done}接口,但还缺少Iterator [optional]的{return,throw}及Iterable的@@iterator()。基于这个不完善的数据集对象,实现一个带有完整的迭代器数据集对象:
1 var obj = { 2 0:"a", 3 1:"b", 4 2:"c", 5 length:3, 6 [Symbol.iterator]:function(){ 7 let curIndex = 0; 8 let next = () => { 9 return { 10 value:this[curIndex], 11 done:this.length == curIndex++, 12 } 13 }; 14 let returnFn = (v) =>{//在for..of迭代循环中使用break中断循环迭代会触发,可以使用迭代对象触发这个方法,根据具体业务需求实现 15 curIndex = this.length; 16 return {value:v,done:true};//这个返回值没有实际意义,v的值为undefined,done也不会作用到迭代器上的遍历 17 } 18 return { 19 [Symbol.iterator](){return this},//返回迭代器Iterator本身,for..of迭代循环就是基于传入对象的的[Symbol.iterator] 20 next, 21 return:returnFn, 22 throw(e){ return new Error(e); }//这个方法暂时没有测试 23 } 24 } 25 }
测试代码1:
1 var it = obj[Symbol.iterator](); 2 for(let p of it){//for..of能使用it作为迭代对象得益于示例中第19行代码,该代码后面有具体解析 3 console.log(p); //打印:a b 4 if(p == "b"){ 5 break; //这里会触发迭代器的return方法 6 } 7 } 8 it.next();//{value: undefined, done: true},触发了return方法修改遍历器位置为最末尾,不能再迭代
测试代码2:
1 var it = obj[Symbol.iterator](); 2 console.log(it.next().value);//a 因为示例中的第19行代码的实现,可以实现单步迭代,还能在这个迭代器后面继续使用for..of继续迭代it 3 for(let p of it){ 4 console.log(p);//b c 5 }
1.构造一个迭代器来生产一个无限斐波那契序列:
1 var Fib = { 2 [Symbol.iterator](){ 3 var n1 = 1, n2 = 1; 4 return { 5 [Symbol.iterator](){ return this; }, 6 next(){ 7 var current = n2; 8 n2 = n1; 9 n1 = n1 + current; 10 return { value:current, done:false }; 11 }, 12 return(v){ 13 console.log("Fibonacci sequence abandoned."); 14 return {value:v, done:true }; 15 } 16 }; 17 } 18 }; 19 for (var v of Fib){ 20 console.log(v); 21 if( v > 50 ) break; 22 } 23 //1 1 3 5 8 13 21 34 55 24 //Fibonacci sequence abandoned.
2.基于迭代器实现一个队列:
1 var tasks = { 2 [Symbol.iterator](){ 3 var arr = this.actions; 4 return { 5 [Symbol.iterator](){ return this; }, 6 next(...args){ 7 if(arr.length > 0){ 8 let res = arr.shift()(...args); 9 return {value: res, done:false} 10 }else{ 11 return {value:undefined, done: true} 12 } 13 }, 14 return(v){ 15 arr.length = 0; 16 return {value:v, done:true}; 17 } 18 }; 19 }, 20 actions:[], 21 add(...funs){ 22 this.actions.push(...funs); 23 } 24 }; 25 function fn1(x){ 26 console.log("step 1:",x); 27 return x * 2; 28 } 29 function fn2(x,y){ 30 console.log("step 2:",x,y); 31 return x + (y *2); 32 } 33 function fn3(x,y,z){ 34 console.log("step 3:",x,y,z); 35 return (x * y) + z; 36 } 37 38 tasks.add(fn1,fn2,fn3); 39 40 var it = tasks[Symbol.iterator](); 41 console.log(it.next(10).value);//step 1: 10 ------ 20 42 console.log(it.next(20,50).value);//step 2: 20 50 -------- 120 43 console.log(it.next(20,50,120).value);//step 3: 20 50 120 -------- 1120 44 console.log(it.next().value);//undefined
3.在Number类原型上定义一个迭代器来表示单个数上的元操作,默认范围是从0到n,这样可以实现一些需要批量的有序数值生成,而且可以通过for..of当个操作数值,或者通过扩展符[...n]来收集这组数值:
1 if(!Number.prototype[Symbol.iterator]){ 2 Object.defineProperty( 3 Number.prototype, 4 Symbol.iterator, 5 { 6 writable:true, //可重写 7 configurable:true,//可配置 8 enumerable:false, //不可枚举 9 value:function iterator(){ 10 var i = 0, inc, done = false, top = +this; 11 inc = this > 0 ? 1 : -1; 12 return { 13 [Symbol.iterator](){return this;}, 14 next(){ 15 inc = i; 16 17 if(top >= 0 ){ 18 i++; 19 }else{ 20 i--; 21 } 22 23 if( Math.abs(inc) > Math.abs(top) ){ 24 done = true; 25 } 26 return {value:inc, done:done} 27 }, 28 return(v){ 29 done = true; 30 return {value:v,done:done} 31 } 32 } 33 } 34 } 35 ); 36 } 37 38 for(let n of 5){ 39 console.log(n); //0 1 2 3 4 5 40 } 41 console.log([...-5]);//[0, -1, -2, -3, -4, -5]
五、迭代器消耗
在第三节中提到过JavaScript原生类自带迭代的有Array,Set,Map,Nodelist,其实还有一个类也自带了迭代器,这个类就是arguments。
Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
除了前面一直作为示例演示的for..of基于迭代器实现,并一次性消耗数据集的所有数据,还有解构(...)数据操作也是基于迭代器实现,并且也同样是一次性消耗数据集所有数据。但是for..of的break可以触发return方法中断迭代器。
迭代器也可以通过手动调用数据集的[Symbol.iterator]()方法获得迭代器,然后以单个数据元的方式消耗迭代器。中途同样可以启动return()方法取消迭代器,如果对这些有疑问的话可以回到前面的示例代码,仔细阅读迭代器API的具体实现就能清晰的了解它为什么可以做得到。