平时我们迭代数据用得最多的应该就是
for
循环了
来看个简单的例子
var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}
如上循环是很简单,但是一旦使用多个循环嵌套时,就需要为每一个循环定义变量,来记录每一次执行迭代时所处集合中的位置,一不小心就会误用了其他for
循环的变量,导致程序错误。然而迭代器可以简化操作,降低程序异常。接下来我们就一起看看什么事迭代器
一、什么是迭代器
迭代器:是一种特殊对象,他具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个
next()
方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value
,表示下一个将要返回的值;另一个是done
,他是一个布尔类型的值,在当没有可返回的数据时返回true
,预示着迭代结束
现在使用ECMAScript 5 语法构建一个迭代器:
function createIterator() {
var i = 0;
return {
next: function () {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
}
}
}
}
var iterator = createInterator([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}"
在这个案例中createIterator()
方法会返回一个对象,该对象拥有一个next()
方法,每次调用next()
方法,都会判断 i
是否大于等于 当前实参的长度;大于或等于时done
即为true
,反之亦然。然后将done
取反;当done为true
时 value
赋值为undefined
;否则将i+1,
再取item
对应下标的值赋值于value
。其实ES6迭代器实现原理和案例也就类似。
二、什么是生成器
生成器是一种返回迭代器的函数, 通过
function
关键字后的星号(*
)来表示,函数中会使用到新的关键字yield
。星号可以紧挨着function
,也可以在中间添加一个空格。
// 在createIterator()前的 * 表示他是一个生成器
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
//生成器的调用和 普通函数相同,只是他返回的是一个迭代器
var iterator = createInterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
需要注意的是,每当执行完一条yield
乬后函数就会自动停止执行。 如上面的案例,当函数初始化执行完yield 1
之后,函数便不会向下执行,直到再次调用迭代器的next()
方法才会继续向下执行yield 2
语句
三、生成器函数表达式
语法
funciont *(){}
在function
关键字和小括号中间添加一个星号
看个案例
let createator = function *(items){
for(let i = 0; i<items.length; i++){
yield iems[i];
}
}
let 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}"
在该案例中createator ()
已经不再是一个普通的函数申明,而是一个生成器函数表达式;
注意:不能使用箭头函数来创建生成器
四、 生成器对象的方法
1、 使用ECMAScript 5 对象字面量风格来定义一个生成器对象
let obj = {
createIterator:function *(items){
for(let i=0; i<items.length; i++){
yield items[i];
}
}
}
let iterator = obj.createIterator([1,2,3]);
其实上面案例可以使用ECMAScript 6 申明函数方法的方式来改写
let obj = {
*createIterator(items){
for(let i=0; i<items.length; i++){
yield items[i];
}
}
}
let iterator = obj.createIterator([1,2,3]);
咋一看这个是不是跟普通的函数申明很像呢;其实*
是可以与方法名留个空格的如* createIterator(items)
;但是我还是感觉在一起好一点(不给他人留任何机会,哈哈)
五、 可迭代对象和 for-of
可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象;Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。在es6中所有集合对象(数组、set和map集合)和字符串都是可迭代对象,这些对象都有默认的迭代器
来访问一下默认的迭代器
let values = [1,2,3];
let iterator = values[Symbol.iterator];
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}"
案例中通过Symbol.iterator
获取数组的values的默认迭代器,并用它遍历数组中的元素。在JavaScript引擎中执行for-of循环语句时也会有类似的处理过程
接下来看个ECMAScript的新特性 for-of
let values = [1,2,3];
for(let num of values){
console.log(num);
}
上面代码会输出
1
2
3
这个 for-of 循环每执行一次都会调用可迭代对象的next()
方法,并将迭代器返回的结果对象的value属性存储在一个变量中,选好将持续执行这一过程知道昂返回对象的done属性的值为true 为止(可以结合‘什么是迭代器’的那一节的案例来理解看你更容易些)
使用Symbol.iterator
来检查对象是否为可迭代对象
function isIterable(object){
return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1,2,3])); // true
console.log(isIterable("hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new weakMap())); // false
console.log(isIterable(new weakSet())); // false
其实上一个案例的for-of循环执行之前也会做类似的对象检查(只是这个是JavaScript引擎做的我们不知道而已)
六、向迭代器传递参数
经过如上案例,已知迭代器可以通过next()向外传值, 也可以通过yield 关键字来生成值,
如果给迭代器的next() 方法传递参数, 则这个参数就会替代生成器内部上一条yield 语句的
返回值。 像要实现像异步编程这样的高级功能,这个迭代器传值的能力会起到很大的作用
function *createIterator(){
let first = yield 1;
let second = yield first + 2;
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{value:1,done:false}"
console.log(iterator.next(4)); // "{value:6,done:false}"
console.log(iterator.next(5)); // "{value:8,done:false}"
console.log(iterator.next()); // "{value:undefined,done:true}"
注意:第一次调用next() 方法无论传入什么参数都会被丢弃,这个是迭代器的一个特性
由于给next() 方法的参数会代替上一次yield 的返回值,而在第一次调用next() 方法前不会
执行执行任何yield语句,因此在第一次调用next() 时传递参数毫无意义
而 next(4) 是将4 作为参数 赋值给了变量 first
这里有个不同就是 在一个含有 yield 的语句中, 表达式右侧等价于 第一次调用next() 方法
后的下一个返回值, 表达式左侧等价于第二次调用next() 方法后,在函数继续执行前得到
的返回值。
iterator.next(4) 方法传入参数为 4, 他会被赋值给变量 first, 函数继续执行…
其中 let first = yield 1;语句 first 值已经是4 了 , 然后语句let second = yield first + 2; 执行了表达式右侧 yield first + 2 计算 出该次调用后的返回值 , 依次类推…
有点绕,修改执行一下代码,看看程序具体执行流程 来加深理解
> function *createIterator(){
... let first = yield 1;
... console.log('first==>',first)
... let second = yield first + 2;
... console.log('second==>',second)
... yield second + 3;
... console.log('final second and first ==>',first,second)
... }
undefined
>
> let iterator = createIterator();
undefined
> console.log(iterator.next());
{ value: 1, done: false }
undefined
> console.log(iterator.next(4));
first==> 4
{ value: 6, done: false }
undefined
> console.log(iterator.next(5));
second==> 5
{ value: 8, done: false }
undefined
> console.log(iterator.next());
final second and first ==> 4 5
{ value: undefined, done: true }
undefined
> console.log(iterator.next());
{ value: undefined, done: true }
undefined
>
七、使用任务迭代器实现异步取值
有了上一步的传值经历,这里我们第一个任务函数来完成 值的向下传递;
看如下dome 将result.value 传入 下一次 next() 方法 直到任务完成
function run(taskDef){
// 创建一个无使用限制的迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 循环调用next()的函数
function step(){
// 如果任务未完成,则继续执行
if(!result.done){
result = task.next(result.value);
step();
}
}
// 开始迭代执行
step();
}
调用上面的方法并传入一个迭代器
run(function*(){
let value = yield 6;
console.log(value); // 6
value = yield value + 3;
console.log(value); // 9
})
看方法调用后输入了 6 和4, 其中 6 取自 yield 6 语句 回传给value 的值,而最后的value 则是取的上一个yield 返回的值加上3
下面就进一步改进上面的方法,完成一个使用迭代器对象完成文件读取的操作
function run(taskDef){
// 创建一个无使用限制的迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 循环调用next()的函数
function step(){
// 如果任务未完成,则继续执行
if(!result.done){
if(typeof result.value === function){
result.value(function(err,data){
if(err){
result = task.throw(err);
return;
}
result = task.next(data);
step();
})
}else{
result = task.next(result.value);
step();
}
}
}
// 开始迭代执行
step();
}
当result.value是一个函数时,任务执行器会先执行这个函数再将结果传入next()方法
如果不是一个函数直接将内容传入next()进行最后的yield返回
定义文件读取 函数
let fs = require('fs');
function readFile(filename){
return function(callback){
fs.readFile(filename,callback);
}
}
// 开始调用执行
run(function *(){
let contents = yield readFile('config.json');
// do something...
doSomethingMethods(contents);
console.log('readFile done.')
})
好了这个方法就如同同步的执行了一个异步方法获取到了文件内容
- 今天先做个简单的了解,明天继续…