《JavaScript高级程序设计》(五) ---迭代器和生成器

迭代器和生成器

迭代器: 一种通用的迭代方式

现有迭代模式

在开始了解迭代器之前,我们先回顾一下现有迭代方式的缺点

css 复制代码
for(let i = 0; i < num; i){}

for循环是最简单的迭代,这种迭代方式有两个明显的缺点:

  • 迭代之前需要知道如何使用数据结构: 迭代时只能先拿到数组的引用,然后使用[]来访问数据
  • 迭代的数据结构相对固定:通过递增所索引来访问数据只能用于数组

es5新增了Array.prototype.forEach,向通用的迭代前进了一步,解决了单独记录索引和通过数组对象取值的问题,但依然有缺点:

  • 不能标识迭代何时中止

在其他语言中使用的模式可以使开发者不需要知道如何迭代而进行迭代操作,这种模式就是迭代器模式,JSes6之后也支持了这种模式

迭代器模式

一种方案,描述什么是可迭代对象:实现了Iterable接口,且该可迭代对象可以通过迭代器Iterator进行消费.

可迭代协议

描述什么是Iterable接口: 以[Symbol.Iterator]为键,以迭代器工厂函数为值,调用这个函数返回一个迭代器对象

该属性作为默认迭代器,再迭代时会先调用该工厂函数,获取到迭代器,然后通过迭代器进行迭代

迭代器协议

描述什么是迭代器: 一种一次性使用的对象,该对象必须包含一个next属性,属性值为一个方法next(),每次调用该方法都会返回一个迭代结果对象result,result对象包含两个属性{value: any,done: Boolean},value表示当此迭代的值,done表示值是否迭代完毕.

找个迭代器实际看一下:

lua 复制代码
以数组为例,数组自带迭代器接口:
​
const arr = ['1','2','3'];
​
const iterator = arr[Symbol.iterator]() // 获取到迭代器对象
// 使用迭代器对象的next方法进行迭代
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}

每调用一次接口生成的迭代器对象都是独立的

自带迭代器的数据结构

js为很多内置类型都实现了迭代器接口,这些数据结构自带默认的迭代器结构,迭代时无需自定义,:

  1. 字符串 String
  2. 数组 Array
  3. 映射 Map
  4. 集合 Set
  5. NodeList DOM集合类型
  6. arguments

隐式调用[Symbol.iterator]

在实际迭代的时候部分语法会隐式调用迭代器接口获取迭代器对象,然后使用迭代器进行迭代,这些方法有:

  1. for of
  2. 数组解构
  3. 扩展运算符
  4. Array.form()
  5. 创建集合
  6. 创建映射
  7. Promise.all(),接收以Promise组成的可迭代对象
  8. Promise.race,接收以Promise组成的可迭代对象
  9. yield*操作符,生成器中使用

自定义迭代器

说了这么多概念,现在来实操一下: 将任何一个object,转换成可迭代对象:

javascript 复制代码
function addIterator(obj){
  const objCopy = _.deepCopy(obj)
  Object.defineProperty(objCopy,Symbol.iterator,{
    enumerable: false,
    writable: false,
    configurable: true,
    value: function(){
      const keys = Object.keys(obj);
      let i = 0;
      const len = keys.length;
      return {
        next: function() {
          return {
            value: obj[keys[i++]],
            done: i > len
          }
        }
      }
    }
  })
  return objCopy 
}
​
let obj = {a: '1',b: '2'};
obj = addIterator(obj) // 获取添加了迭代器接口的对象
​
const iterator = obj[Symbol.iterator]() // 获取迭代器
// 尝试打印一下
console.log(iterator.next()) // {value: 1,done: false}
console.log(iterator.next()) // {value: 2,done: false}
console.log(iterator.next()) // {value: undefined,done: true}
​
for(let value of obj){
    console.log(value) // 1,2
}

提前中止迭代器

迭代器对象还有一个可选的return方法,在迭代器,在迭代提前退出时调用,并且该方法必须返回一个有效的IteratorResult对象,简单情况下也可以返回一个{done: true}

那么什么叫做提前退出呢? 包括一下情况:

  • for-of循环通过break,continue,return,throw提前退出的情况
  • 解构操作并未消费所有的值

对这几种情况来实验一下:

kotlin 复制代码
以break为例子:
const arr = [1,2,3,4,5,6];
const iterator = arr[Symbol.iterator]()
iterator.return = function(){
  console.log('提前退出')
  return {done: true}
}
for(let val of iterator){
  if(val === 2){
    break
  }
  console.log(val)
}
// 输出:
// 1 
// 提前退出

不仅如此迭代器还有关闭的概念,如果一个迭代器没有关闭,还可以继续从上次迭代的地方继续迭代,被关闭了则不可以.JS中有的迭代器可以关闭,有的迭代器不会关闭,比如数组:

kotlin 复制代码
const arr = [1,2,3,4];
const iterator = arr[Symbol.iterator]()
iterator.return = function(){
  console.log('提前退出')
  return {done: true}
}
for(let val of iterator){
  if(val === 2){
    break
  }
  console.log(val)
}
// 1 
// 提前退出
for(let val of iterator){ // 由于数组的迭代器不能关闭,本次的迭代会继续上次的位置开始
  if(val === 2){
    break
  }
  console.log(val)
}
// 3
// 4

需要注意的是迭代器能否关闭和有无return方法无关

生成器 Generator

了解生成器之前先思考一下:

  • 函数执行的过程中可以返回多个值吗?
  • 函数执行的过程中可以接收外界的参数吗?

正常函数当然是不可以的,但生成器函数可以实现这两点,因为他有特殊的关键字yield,下面我们来研究一下它是怎么做到的

什么是生成器

一个由function*定义的函数,调用这个函数返回一个迭代器对象,并且函数内部可以使用关键字yield暂停代码的执行:

javascript 复制代码
执行代码遇到yield将暂停函数的执行,并且函数的上下文不会被销毁
function* generatorFn(){
    yield 1;
    return 0;
}

生成器函数具有以下特征:

  • 调用生成器函数不会立即执行函数内部代码 ,而是返回一个生成器对象,包含迭代器对象的所有特征,因此生成器对象可以作为迭代器对象使用
  • 调用生成器对象next方法才会执行生成器函数中的代码,且next函数返回结果为iteratorReault对象:{value:any,done: Boolean},value表示执行结果,done表示迭代是否完毕
  • 执行代码时,遇到yield关键字,将暂停函数的执行,并将yield后的表达式的值返回给iteratorReaultvalue属性,本次next函数执行完毕
  • 再次调用next方法时,生成器函数会从上个yield暂停的地方继续执行...循环往复
  • 直到没有yield或遇到return时,done自动变为true,表示生成器函数执行完毕,之后调用next都返回{value:undefined,done: true}
lua 复制代码
function* generatorFn(){
    yield 1;
    yield 2;
    yield 3
    return 0;
}
​
const generator = generatorFn() // 获取生成器对象
generator.next() // 开始执行生成器函数内部代码,遇到yield关键字结束,并返回yield后表达式的值 这里输出 {value: 1,done: false}
generator.next() // 继续从上次暂停的yield向下执行,遇到第二个yield暂停执行,输出 {value: 2,done: false}
generator.next() // 从第二个yield之后开始执行,遇到第三个yield暂停执行,输出{value: 3,done: false}
generator.next() // 从第三个yield之后开始执行,遇到renturn 输出{value: 0,done: true}
generator.next() // 已经没有代码可以执行了,之后调用next都将返回{value: undefined,done:true}

总结一下:调用生成器函数返回 一个生成器对象,每调用一次生成器对象next方法就会将生成器函数内的代码执行到下一个yield,并返回一个{value:any,done:boolean}对象,value表示yield后表达式的值,done表示迭代是否结束,继续调用将重复上述步骤

yield关键字

yield关键字相当于一个暂停标记,函数执行到yield时将暂停执行,而调用next方法会恢复函数的执行,yield有以下特点:

  • 只能在生成器函数的顶层作用域使用
  • 函数执行到yield时,将暂停函数执行,并将紧跟yield关键字的表达式的值返回给next执行结果的value属性
与return的异同:
  • 相同点

    都会返回紧跟在后面表达式的值

  • 不同点

    代码遇到yield会暂停,并在下次执行时从该位置继续执行

    代码遇到return就执行结束了.

由于yield关键字的存在,生成器函数可以返回多个值,就像一个个生成值一样,这也是生成器名字的由来,正是因为该关键字,让生成器函数可以在函数执行过程中返回多个值出来

yield*

在生成器内部调用另一个生成器时,一般做法是通过for of迭代另一个生成器:

javascript 复制代码
function* generatorFn(){
    yeild 1 + 2;
    for(let key of otherGeneratorFn()){ // 手动遍历
        yield key
    }
    return 8
}

这样无疑很繁琐,不过es6提供了yield*来解决这个问题:

javascript 复制代码
function* generatorFn(){
    yeild 1 + 2;
    yield* otherGeneratorFn()
    return 8
}

实际上**yield*可以迭代任何可迭代对象**,这样边可以轻松实现生成器的递归操作

next方法的参数

yield本身没有返回值,或者说他的返回值是undefined,如下例所示:

vbnet 复制代码
function* generatorFn(){
    const a = yeild 1 + 2;
    return a
}
​
const generator = generatorFn()
generator.next() // 执行到第一个yield暂停函数的执行,并返回yield后表达式的值: {value: 3,done: false}
generator.next() // 继续上个yeild 将yeild本身的值赋值给a,执行到return,函数结束返回a的值: {value: undefined,done: true}

next方法可以携带一个参数,这个参数会作为上一次让函数暂停的 yield的返回值:

vbnet 复制代码
function* generatorFn(){
    const a = yeild 1 + 2;
    return a
}
​
const generator = generatorFn()
generator.next(2) // 虽然传递参数了,依然输出{value: 3,done: false},因为参数只传给上次让函数暂停的yield,这是第一次执行,因此上次的yield是不存在的
generator.next(2) // 输出{value: 2,done:true},next的参数将传递给上次暂停的yield,因此代码`yeild 1+2`中yield的值为2,赋值给a后由return返回

与迭代器接口的关系

上文说过: 生成器函数执行时返回一个生成器对象,生成器对象可以当作迭代器对象使用,所以说生成器函数其实就是一个迭代器工厂函数,因此它可以直接赋值给迭代器接口[Symbol.iterator],作为迭代器工厂函数使用,实际上由于yield的存在,使用生成器作为迭代器工厂函数更加方便,

还记得上文中实现自定义迭代器的例子吗?现在使用生成器重写一下用以比较:

javascript 复制代码
// 将一个object变成可迭代对象
function addIterator(obj){
  const objCopy = _.deepCopy(obj)
  Object.defineProperty(objCopy,Symbol.iterator,{
    enumerable: false,
    writable: false,
    configurable: true,
    value: function* genertorfn(){
        const keys = Object.keys(objCopy);
        for(let key of keys){
            yield objCopy[key]
        }
    }
  })
  return objCopy 
}
​
let obj = { a: 1, b: 2 };
obj = addIterator(obj);
for (let value of obj) {
    console.log(value); // 1 2
}

可以看到没有嵌套,没有闭包就可以实现,并且可读性更强

提前终止生成器

调用生成器函数返回一个生成器对象,该对象也有关闭的概念,如果进入关闭状态就无法恢复了,后续调用next返回donetrue,提供的任何返回值都不会被存储或传递.

迭代器对象有的是可以被关闭的,有的不可以,而生成器不同,可以强制进入关闭状态,他是如何做的呢? 主要通过它的两个方法return()throw()

return

迭代器对象的return方法是可选的,而每个生成器都自带return方法,调用该方法就可以使生成器函数进入关闭状态:

javascript 复制代码
function* generatorFn(){
    yield 1;
    yield 2;
    yield 3;
    return 4
}
const generator = generatorFn()
generator.next() // {value: 1,done: false}
generator.return() // {value: undefined,done: true} 强制关闭生成器
generator.next() // {value: undefined,done: true} 关闭后将不再迭代

return方法还可以传递一个参数,该参数最终传递给迭代结果的value属性,上述例子稍微修改一下:

yaml 复制代码
generator.next() // {value: 1,done: false}
generator.return(4) // {value: 4,done: true} 强制关闭生成器,value为return传递的值
throw

调用该方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被生成器内部处理,生成器就会关闭

javascript 复制代码
function* gen() {
    while (true) {
      yield 42;
    }
}
// 外部处理错误,即使捕获了也会关闭生成器
const g = gen();
console.log("g.next();:", g.next());// { value: 42, done: false }
try {
    g.throw(new Error("出现了些问题"));
} catch (e) {
    console.log("捕获到外部错误");   // 捕获到外部错误
}
console.log("g.next();:", g.next()); // { value: undefined, done: true }
​
//内部捕获错误,生成器不会被关闭
function* gen() {
    while (true) {
      try {
        yield 42;
      } catch (e) {
        console.log("捕获到错误!");
      }
    }
}
​
const g = gen();
console.log("g.next();:", g.next());// { value: 42, done: false }
g.throw(new Error("出现了些问题")); // "捕获到错误!"
console.log("g.next();:", g.next());// { value: 42, done: false }
彻底理解next,return,throw

这三个生成器属性有一个共同点: 都可以看作是用来替换yield和后面的表达式的

  • next是将yield表达式替换为一个值
vbnet 复制代码
// next:
function* generatorFn(){
    let a = yield 1;
    let b = yield 2;
    yield 3;
    return 4
}
const generator = generatorFn()
generator.next()
generator.next('哈')
//上面的next相当于:
//将 let a = yield 1中yield替换为 let a = '哈'
  • return是将yield替换为return语句
vbnet 复制代码
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
  • throw是将yield替换为throw语句
vbnet 复制代码
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

强烈推荐:自己感觉比javascript高级程序设计描述的更易懂且全面: 阮一峰Grnerator

相关推荐
花生侠21 分钟前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
猿榜22 分钟前
魔改编译-永久解决selenium痕迹(二)
javascript·python
阿幸软件杂货间27 分钟前
阿幸课堂随机点名
android·开发语言·javascript
一涯28 分钟前
Cursor操作面板改为垂直
前端
我要让全世界知道我很低调35 分钟前
记一次 Vite 下的白屏优化
前端·css
threelab36 分钟前
three案例 Three.js波纹效果演示
开发语言·javascript·ecmascript
1undefined237 分钟前
element中的Table改造成虚拟列表,并封装成hooks
前端·javascript·vue.js
蓝倾1 小时前
淘宝批量获取商品SKU实战案例
前端·后端·api
comelong1 小时前
Docker容器启动postgres端口映射失败问题
前端