likes
comments
collection
share

JavaScript高级--迭代器和生成器

作者站长头像
站长
· 阅读数 26

迭代器

  • 迭代器(iterator)是确使用户可在容器对象(如链表、数组)上进行遍历访问的对象,使用该接口无需关心对象的内部实现细节
  • 迭代器的行为像数据库中的光标,最早出现在 1974 年设计的 CLU 编程语言中,在现代编程语言(如 JavaJavaScript 等)的实现中,迭代器的实现方式各不相同
  • JavaScript 中,迭代器是一个具体的对象,该对象需要符合迭代器协议(iterator protocol)
  • 迭代器协议: 定义了产生一系列值的标准方式,JavaScript 中的标准就是特定的 next 方法

标准的 next 函数要求:一个无参数函数,返回拥有以下两个属性的对象

  • done(boolean):

    • 若迭代器可以产生序列中的下一个值则为 false
    • 若迭代器已将序列迭代完毕则为 true,这种情况下 value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值
  • value: 迭代器返回的任何 JavaScript 值,done 属性为 true 时可省略

JavaScript高级--迭代器和生成器

 const iterator = {
   next() {
     return { done: false, value: 123 };
   }
 };
  • 举个例子: 创建一个迭代器访问数组内容
 const names = ['Jimmy', 'James', 'Joy'];
 ​
 let index = 0;
 ​
 const nameIterator = {
   next() {
     if (index < names.length) {
       return { done: false, value: names[index++] };
     } else {
       return { done: true, value: undefined };
     }
   }
 };
 ​
 nameIterator.next(); // { done: false, value: Jimmy }
 nameIterator.next(); // { done: false, value: James }
 nameIterator.next(); // { done: false, value: Joy }
 nameIterator.next(); // { done: true, value: undefined }
 nameIterator.next(); // { done: true, value: undefined }

创建一个生成迭代器的函数

  • 如果对于每个容器对象都写一次对应的迭代器,那么就会造成很多代码重复
  • 可以针对某种容器对象(如数组)创建一个生成迭代器的函数
 function creaetArrayIterator(arr) {
   let index = 0;
   return {
     next() {
       if (index < arr.length) {
         return { done: false, value: arr[index++] };
       } else {
         return { done: true, value: undefined };
       }
     }
   };
 }
 ​
 // 使用函数生成对应的迭代器
 const namesIterator = creaetArrayIterator(['Jimmy', 'James', 'Joy']);
 const numsIterator = creaetArrayIterator([10, 20, 30]);

可迭代对象

可迭代对象和迭代器是不同的概念

  • 当一个对象实现了可迭代协议(iterable protocol)时,它就是一个可迭代对象
  • 可迭代对象要求必须实现 @@iterator 方法,在代码中使用 Symbol.iterator 访问该属性

JavaScript高级--迭代器和生成器

 const iterableObj = {
   [Symbol.iterator]: function(){
     return iterator;
   }
 }
  • 使用可迭代对象生成一个迭代器,将相关的部分都耦合在一个可迭代对象中
 const iterableObj = {
   names: ['Jimmy', 'James', 'Joy'],
   [Symbol.iterator]: function () {
     let index = 0;
     return {
       next: () => {
         if (index < this.names.length) {
           return { done: false, value: this.names[index++] };
         } else {
           return { done: true, value: undefined };
         }
       }
     };
   }
 };
 ​
 const iterator = iterableObj[Symbol.iterator]();
 iterator.next(); // { done: false, value: Jimmy }
 iterator.next(); // { done: false, value: James }
 iterator.next(); // { done: false, value: Joy }
 iterator.next(); // { done: true, value: undefined }
  • 以上方式在每次调用 iterableObj[Symbol.iterator]() 时,都会生成一个新的迭代器

for...of 与可迭代对象的关联

  • 当使用 for...of 进行遍历时,遍历的对象必须是可迭代对象,普通对象则不行
 // 这种用法则会报错
 const obj = { a: 1, b: 2 };
 for (const item of obj) {
   // ...
 }
  • 那么可以自定义一个可迭代对象,提供于 for...of 遍历,其所遍历的结果是迭代器每次调用 next() 返回的 value
  • for...of 可以看成是一种语法糖,相当于 iterator.next().value,当 done 属性为 false 时,就取其 value 值赋给 item(如下所示)
 for (const item of iterableObj) {
   console.log(item); // Jimmy, James, Joy;
 }

迭代器的中断

迭代器可在没有完全迭代的情况下中断

  • 遍历过程中使用 breakcontinuereturnthrow 等中断循环
  • 在解构时,没有解构所有的值

监听迭代器的中断

  • 迭代器中断时,会调用其内部实现的 return 方法
 const iterableObj = {
   names: ['Jimmy', 'James', 'Joy'],
   [Symbol.iterator]() {
     let index = 0;
     return {
       next: () => {
         if (index < this.names.length) {
           return { done: false, value: this.names[index++] };
         } else {
           return { done: true, value: undefined };
         }
       },
 ​
       return: () => {
         console.log('迭代器被中断了');
         return { done: true, value: undefined };
       }
     };
   }
 };
 ​
 // 当循环被break时,会触发迭代器的return方法
 for (const value of iterableObj) {
   if (value === 'James') break;
 }

生成器

  • 生成器是 ES6 中新增的一种函数控制、使用的方案,能灵活地控制函数的执行时机,它允许定义一个有迭代算法的函数,同时可以自动维护自身状态
  • 生成器事实上是一种特殊的迭代器

执行流程对比

普通函数执行流程

  • 普通函数的执行流程的同步的,由上往下依次执行内部代码,不可中断
 function foo() {
   const var1 = 100;
   console.log(var1); // 100
 ​
   const var2 = 200;
   console.log(var2); // 200
 }
 ​
 foo(); 
 ​
 console.log(300); // 300
  • 如上所示,上面代码会先把函数内部逻辑执行完成,再执行最底下的 console.log
  • 那么有没有办法,可以使得函数先输出 100,然后暂停往下执行内部代码,而是待输出 300 后,再回到函数内部执行剩余代码,接着输出 200

生成器函数执行流程

  • 利用生成器实现对函数的执行时机进行控制
 function* generatorFoo() {
   const var1 = 100;
   console.log(var1);
   yield; // 函数暂停
 ​
   const var2 = 200;
   console.log(var2);
 }
 ​
 const generator = generatorFoo(); // 初次调用函数不执行,而是返回一个Geneator对象
 generator.next(); // 函数执行,输出100
 console.log(300);
 generator.next(); // 函数执行,输出200
  • 如上所示,当使用生成器函数时,函数内部的执行逻辑可以被暂停,进而把控制权交到函数外部,执行其他代码,所以最后的输出顺序就会是 100、300、200

对比普通函数

生成器函数和普通函数的区别

  • ①生成器函数需要在 function 关键字后加一个符号 *
  • ②生成器函数可通过 yield 关键字来控制函数的执行流程
  • ③生成器函数在最初调用时不执行任何代码,而是返回一个名为 Generator 的特殊迭代器,需通过调用生成器的 next 方法时,生成器函数才执行,直到遇到 yield 关键字则暂停

执行过程

  • 如下代码所示,它的执行顺序并不像普通函数一样,从上往下依次执行,而是通过 next 方法和 yield 关键字相互配合,对 generatorFoo 函数进行控制
 function* generatorFoo() {
   const var1 = 100;
   console.log(var1);
   yield;
 ​
   const var2 = 200;
   console.log(var2);
   
   return123
 }
 ​
 const generator = generatorFoo();
 const res1 = generator.next();
 console.log(300);
 const res2 = generator.next();
  • 首先调用 generatorFoo() 函数,但函数内部代码并不执行,而是返回一个名为 Generator 的特殊迭代器
 const generator = generatorFoo(); // 调用函数
  • 调用 generator.next() 方法,执行函数内部代码
 const res1 = generator.next(); // 函数执行
  • 函数内部代码执行后,遇到 yeild 语句,函数暂停,并且会返回一个对象

返回的对象和迭代器中的形式一致,如 { value: xxx, done: false }

  • value 值取决于 yield 语句后的内容,如 yield 123;value 值为 123,相当于不中断的 return
  • done 值取决于函数内部代码是否已执行完毕,是则为 true,反之为 false
 const var1 = 100;
 console.log(var1); // 输出100
 yield; // 遇到yield,函数暂停,并返回 { value: undefined, done: false }
 ​
 ​
 // res1 = { value: undefined, done: false }
  • 函数暂停,继续执行全局代码
 console.log(300); // 输出300
  • 继续调用 generator.next() 方法,返回函数中,继续从上次暂停的 yield 开始往下执行
 const res2 = generator.next(); // 函数执行
  • 执行函数内后面的代码,当执行结束后,同样返回一个对象

这次对象内容则为 { value: 123, done: true }

  • value 值为 123 是因为函数最后 return 了 123,return 相当于一个特殊的 yield 语句
  • done 值为 true是因为函数已经 return,表示执行结束
 const var2 = 200;
 console.log(var2); // 输出200
 ​
 return 123
 ​
 // res1 = { value: 123, done: true }

总结

  • 生成器是一种特殊的迭代器
  • 生成器函数当遇到 yield 语句时,函数暂停;当遇到 return 语句时,生成器停止

其他方法

next 方法传递参数

  • 生成器的 next 方法是可以传参数的,如下代码所示,当第二次调用 next 函数时传入参数
 function* generatorFoo() {
   yield;
 ​
   const var2 = 200;
   console.log(var2);
 }
 ​
 const generator = generatorFoo();
 generator.next();
 generator.next(10);
  • next 方法传入的参数,会被上一个 yield 语句所接收,如下所示
 function* generatorFoo() {
   const num = yield; // 接收第二次next传入的参数10
 ​
   const var2 = 200 * num
   console.log(var2); // 2100
 }
 ​
 const generator = generatorFoo();
 generator.next();
 generator.next(10);
  • 注意: 第一次调用 next 传参是无意义的,因为没有 yield 语句接收,若想接收参数可在调用生成器函数 generatorFoo 时传入
 function* generatorFoo(num) {
   yield;
 ​
   const var2 = 200 * num;
   console.log(var2);
 }
 ​
 const generator = generatorFoo(10);
 generator.next(); // { value: 2000, done: true }

return 方法终止生成器

  • 调用生成器的 return 方法可以提前结束生成器,而函数后面代码不会继续执行
 function* generatorFoo() {
   const var1 = 100;
   console.log(var1);
   const num = yield var1;
 ​
   const var2 = 200 * num;
   console.log(var2);
   yield var2;
 ​
   console.log('end');
 }
 ​
 const generator = generatorFoo();
 generator.next();
 generator.return(10);
  • 如上所示,当调用 generator.next() 时,函数执行,输出 100,遇到 yield 语句暂停
  • 当调用 generator.return(10) 时,生成器终止,函数后面代码都不会执行
 function* generatorFoo() {
   ...
   const num = yield var1;
   
   // 以下代码都不会执行
   const var2 = 200 * num;
   console.log(var2);
   yield var2;
 ​
   console.log('end');
 }
  • 此时 generator.return(10) 的返回值是 { value: 10, done: true },相当于在未执行的代码前加上了一个 return 语句
 function* generatorFoo() {
   ...
   const num = yield var1; 
   return num; // 调用return方法相当于加上一个return语句,而return传入的参数被上一个yield接收
   
   // 以下代码都不会执行
   const var2 = 200 * num;
   console.log(var2);
   yield var2;
 ​
   console.log('end');
 }

throw 方法抛出异常

  • 生成器的 throw 方法也会终止生成器,与 return 不同的是,调用 throw 方法,上一个 yield 语句会抛出异常
 function* generatorFoo() {
   const var1 = 100;
   console.log(var1);
   try {
     yield var1;
   } catch (e) {
     console.log('捕获到异常:', e);
   }
 ​
   const var2 = 200;
   console.log(var2);
   yield var2;
 ​
   console.log('end');
 }
 ​
 const generator = generatorFoo();
 generator.next();
 generator.throw();
  • yield 语句抛出的异常可以被捕获,捕获异常后可以继续执行函数的后面代码
  • 若不捕获 yield 语句的异常,生成器则会终止

JavaScript高级--迭代器和生成器

替代迭代器

  • 既然生成器是一种特殊的迭代器,那么在某些情况下,可以使用生成器来替代迭代器
  • 之前使用迭代器,对某个容器对象进行迭代,都需要定义 indexnext 方法
 function creaetArrayIterator(arr) {
   let index = 0;
   return {
     next() {
       if (index < arr.length) {
         return { done: false, value: arr[index++] };
       } else {
         return { done: true, value: undefined };
       }
     }
   };
 }
  • 可以使用生成器函数进行替代,然后调用其返回的 Generator 对象的 next 方法
 function* createArrayIterator(arr) {
   for (const item of arr) {
     yield item;
   }
 }
  • 事实上还可以使用 yield* 语句来生产一个可迭代对象,yield* 相当于一种语法糖,它会依次迭代这个可迭代对象,每次迭代其中的一个值
 function* createArrayIterator(arr) {
   yield* arr;
 }
 ​
 const namesIterator = createArrayIterator(['Jimmy', 'James', 'Joy']);
 ​
 namesIterator.next(); // { value: 'Jimmy', done: false }
 namesIterator.next(); // { value: 'James', done: false }
 namesIterator.next(); // { value: 'Joy', done: false }
 namesIterator.next(); // { value: undefined, done: true }