Iterator和Generator

58 阅读9分钟

Iterator

遍历器(Iterator)是一种机制(接口):为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署Iterator接口,就可以完成遍历操作【for of 循环本身就是基于这种机制进行迭代的】,依次处理该数据结构的所有成员

  • 拥有next方法用于依次遍历数据结构的成员
  • 每一次遍历返回的结果是 一个对象{done:false,value:xxx} ;done:记录是否遍历完成,value:当前遍历的结果

根据这两个特点实现一个Iterator类

// 简单实现一个Iterator
class Iterator{
  constructor(assemble){
    let self = this;
    self.assemble=assemble;
    self.index=0;
  };
  next() {
     let self = this,
     assemble=self.assemble;
     if(self.index>assemble.length-1){
       return{
         value:undefined,
         done:true,
       }
     }
     return {
       value:assemble[self.index++],
       done:false,
     };
  };
}
let itor = new Iterator([10,20,30,40])
console.log(itor.next());//{value: 10, done: false}
console.log(itor.next());//{value: 20, done: false}
console.log(itor.next());//{value: 30, done: false}
console.log(itor.next());//{value: 40, done: false}
console.log(itor.next());//{value: undefined, done: true}

拥有Symbol.iterator属性的数据结构(值),被称为可被遍历的,可以基于for of 循环处理

虽然不具备Iterator内置类,但是一些内置类型拥有默认的迭代器行为它提供了了Symbol.iterator属性方法,这个方法具备迭代器规范,基于这个方法可以依次迭代数据中的每一项

mdn搜索如下:

\

  • 数组  Array.prototype[Symbol.iterator]
let arr = [10,20,30];
let itor = arr[Symbol.iterator](); 
// 1.先去找arr[Symbol.iterator]执行(如果数据集合不具备这个属性,就会报错【Uncaught TypeError: {} is not iterable】),否则返回一个迭代器对象itor
// 2.每一轮循环都会执行一次itor.next()方法,把结果中的value赋值给循环中的value;当done 为true时,则结束整个循环
console.log(itor.next());//{value: 10, done: false}
console.log(itor.next());//{value: 20, done: false}
console.log(itor.next());//{value: 30, done: false}
console.log(itor.next());//{value: undefined, done: true}
// 数组是一个可以被迭代的数据,但并不是一个迭代器,它的原型上有Symbol.iteratorlet arr=[10,20,30];
arr.next();// arr.next is not a function
// 所以先变成迭代器


let arr = [10,20,30];
let itor = arr[Symbol.iterator](); 
console.log(itor)

\

\

  • 部分类数组:arguments/NodeList/HTMLCollection ...
  • String
var string = 'A\uD835\uDC68';

var strIter = string[Symbol.iterator]();//返回一个新的 Iterator 对象。

console.log(strIter.next().value); // "A"
console.log(strIter.next().value); // "\uD835\uDC68"
  • Set
  • Map
  • generator object

普通对象默认不具备Symbol.iterator ,属于不可被遍历的数据结构\

如果每次迭代,都需要一直next,所以for of 就是基于这个机制去迭代的(这也就是for of 的原理)

// 普通对象是不具备这个属性的(Object.prototype上不具备Symbol.iterator),所以用for of 循环会直接报错 【Uncaught TypeError: obj is not iterable】
let obj={name:'jiang',age:14}
for(value of obj){
  console.log(value)
}

如果想让对象也可迭代,那就给对象也加一个Symbol.iterator属性

let obj={name:'jiang',age:14};
obj[Symbol.iterator]=function(){
    let self = this,
    keys=Reflect.ownKeys(self),// ["name", "age"]由目标对象的自身属性键组成的 Array
    index=0;
    return{
      next(){
        if(index>keys.length-1){
          return{
            value:undefined,
            done:true,
          }
        }
        return{
            value:self[keys[index++]],
            done:false,
        }
      }
    }
}
console.log(obj)for(value of obj){
  console.log(value)
}

\

\

如果想让所有对象都能用,加在原型上,那所有的对象都可以用for of 来循环了 \

Object.prototype[Symbol.iterator]=function(){
    let self = this,
    keys=Reflect.ownKeys(self),
    index=0;
    return{
      next(){
        if(index>keys.length-1){
          return{
            value:undefined,
            done:true,
          }
        }
        return{
            value:self[keys[index++]],
            done:false,
        }
      }
    }
}
let obj={name:'jiang',age:14};
for(value of obj){
  console.log(value)
}

类数组和数组非常相似,可以把数组上的属性借用给类对象,也能用for of\

let obj={
  0:10,
  1:20,
  2:30,
  length:3,
}
obj[Symbol.iterator]= Array.prototype[Symbol.iterator];
for(value of obj){
  console.log(value)
}

扩展:js中的常用的循环,以及它们的性能对比

for循环、while循环、Array.prototype.forEach可以迭代数组、for of循环、for in 循环

let arr=new Array(9999999).fill(null);
console.time('FOR')
for(var i=0;i<arr.length;i++){
}
console.timeEnd('FOR');

console.time('while')
let k=0;
while(k<arr.length){
  k++;
}
console.timeEnd('while');

console.time('FOREACH')
arr.forEach(()=>{})
console.timeEnd('FOREACH');

console.time('FOROF')
for(let item of arr){}
console.timeEnd('FOROF');

console.time('FORIN')
for(let item in arr){}
console.timeEnd('FORIN');

\

性能从高到低:

for 循环&&while循环->forEach循环(内部封装的方法,肯定做了一些逻辑判断,稍微慢了点)->for/of循环->for/in 循环(会依次查找私有和原型链上的所有可枚举属性,所以比其他循环要慢)

Generator

\

Generator 函数是 ES6 提供的一种异步编程解决方案,它的特点\

  • 一是,function关键字与函数名之间有一个星号\

  • 二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
console.log(hw)

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。

不同的是,调用 Generator 函数后,该函数并不执行,如果传有实参,实参值会预先传进去,返回的也不是函数运行结果,返回一个具备迭代器规范的对象\

\

通过原型链指向一层一层往上找,找到GeneratorFunction,里面有next/return/throw方法

(1)Generator.prototype.next()

next() 方法返回一个包含属性 done 和 value 的对象。该方法也可以通过接受一个参数用以向生成器传值。

  • value可选,向生成器传递的值.(如果传递了该参数,那么这个参数会传给上一条执行的 yield语句左边的变量

  • 返回值:返回的对象包含两个属性:

    • done (布尔类型)

      • 如果迭代器超过迭代序列的末尾,则值为 true。 在这种情况下,value可选地指定迭代器的返回值。
      • 如果迭代器能够生成序列中的下一个值,则值为false。 这相当于没有完全指定done属性。
    • value 迭代器返回的任意的Javascript值。当 done 的值为 true 时可以忽略该值。

function* generator() {
    let a = yield 1;
    let b = yield 2;
    let c = yield 3;
    console.log(`a = ${a}, b = ${b}, c = ${c}`)
}

let gen = generator(); // "Generator { }"
console.log(gen.next(10));  // {value: 1, done: false}
console.log(gen.next(9));  // {value: 2, done: false}
console.log(gen.next());  // {value: 3, done: false}
console.log(gen.next(8));  
// a = 9, b = undefined, c = 8
// {value: undefined, done: true}

(2)Generator.prototype.return()

return() 方法返回给定的值并结束生成器。
gen.return(value)

  • value: 需要返回的值
  • 返回值:返回该函数参数中给定的值.

(3)Generator.prototype.throw()

throw() 方法用来向生成器抛出异常,并恢复生成器的执行,返回带有 done 及 value 两个属性的对象。\

gen.throw(exception)
  • exception:用于抛出的异常。

  • 返回值:带有两个属性的`对象

    • done (boolean)

      • 如果迭代器已经返回了迭代序列的末尾,则值为 true。在这种情况下,可以指定迭代器 value 的返回值。 
      • 如果迭代能够继续生产在序列中的下一个值,则值为 false。 这相当与不指定 done 属性的值。
    • value - 迭代器返回的任何 JavaScript 值。当 done 是 true 的时候可以省略。

function* gen() {
    while(true) {
      try {
         yield 42;
      } catch(e) {
        console.log("Error caught!");
      }
    }
  }
  
  var g = gen();
  console.log(g.next()); // { value: 42, done: false }
  console.log(g.throw(new Error("Something went wrong"))); // "Error caught!"  { value: 42, done: false }
  console.log(g.next()); // { value: 42, done: false }

从上述代码中可以看出,关于throw()方法需要注意的是,throw()方法并\color{red}{不会结束生成器}不会结束生成器。(如果不通过try catch 捕获,直接就会结束生成器)

具体执行方式:\

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next());//{value: 'hello', done: false}
console.log(hw.next());//{value: 'world', done: false}
console.log(hw.next());//{value: 'ending', done: true}
console.log(hw.next());//{value: undefined, done: true}

第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束。\

第二次调用如上,

第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)\

第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。\

注意:

// ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。

function * foo(x, y) { }
function *foo(x, y) { }
function* foo(x, y) { }
function*foo(x, y) { }

Iterator与Generator关系:

iterator迭代对象和generator实例一样都使用next方法迭代 ,并且这个 yield 语句就是用来中断代码的执行的,也就是说,配合 next() 方法,每次只会执行一个 yield 语句

基于生成器函数执行的返回结果就是一个迭代器\

function * gen(){

}
const f=gen();
console.log(f)
f.next();

之前给Object原型上加的Symbol.iterator的方法,可以用生成器来实现(生成器函数执行就是迭代器对象)

Object.prototype[Symbol.iterator] = function*() {
  for (const [key, value] of Object.entries(this)) {
    yield { key, value };
  }
};
// 具备这个属性,就可以用for of 来循环对象


for (const { key, value } of { a: 1, b: 2, c: 3 }) {
  console.log(key, value);
}

generator的应用

因为有这个暂停特点(把函数执行打断,看上去是同步的,实际是异步的),在真实项目中就可以来管理异步编程: 

使用generator,让异步代码像同步代码一样执行

// 串行发送三个请求,请求时间分别是1000、2000、3000

function query(interval){
  return new Promise(resolve=>{
    setTimeout(()=>{
      resolve(interval);
    },1000)
  })
}

// Promise的出现,让程序员告别了痛苦的回调地狱,书写异步代码像这样:

query(1000).then(value=>{
  console.log('第1个成功,',value);
  return query(2000);
}).then(value=>{
  console.log('第2个成功,',value);
  return query(3000);
}).then(value=>{
  console.log('第3个成功,',value);
})

// 用Generator实现,手动执行next


function * generator(){
  let value=yield query(1000);
  console.log('第1个成功,',value);
  value=yield query(2000);
  console.log('第2个成功,',value);
  value=yield query(3000);
  console.log('第3个成功,',value);
}
let itor=generator();

// itor.next(); // value:第一次请求的promise实例 {value: Promise, done: false},接下来需要等待本次请求成功,获取到结果 


itor.next().value.then(val=>{
  // val:第一次请求成功的结果
  itor.next(val).value.then(val=>{
    //val:第二次请求成功的结果
    itor.next(val).value.then(val=>{
      //val:第三次请求成功的结果
      itor.next(val);
    })
  })
})

// 用Generator实现,自动执行next function query(interval){
   return new Promise(resolve=>{
    setTimeout(()=>{
      resolve(interval);
    },1000)
  })
 }
  function AsyncFunction(generator,...params){
  let itor=generator(...params);
  const next=(val)=>{
    let {value,done}=itor.next(val);
    if(done) return; // done为true结束
    value.then(val=>next(val))
      .catch(reason=>itor.throw(reason));
  }
  next();
}

AsyncFunction(function * (){
  let value=yield query(1000);
  console.log('第1个成功,',value);
  value=yield query(2000);
  console.log('第2个成功,',value);
  value=yield query(3000);
  console.log('第3个成功,',value);
})

// 有个co模块就是用类似这种方式

// async await是generator+promise的‘语法糖’,就是将generator函数的*换成async,用await替代yeild关键词。不用再考虑generator实例如何调用next方法了

function query(interval){
  return new Promise(resolve=>{
    setTimeout(()=>{
      resolve(interval);
    },1000)
  })
}
(async()=>{
  let result=await query(1000);
  console.log('第1个成功,',result);
  result=await query(2000);
  console.log('第2个成功,',result);
  result=await query(3000);
  console.log('第3个成功,',result);
})();

参考:

[迭代协议]developer.mozilla.org/zh-CN/docs/…

[Generator]developer.mozilla.org/zh-CN/docs/…

es6.ruanyifeng.com/#docs/gener…

\