【JS进阶】JS 迭代器和生成器

JS 迭代器和生成器

1.迭代器

迭代器,Iterator,其代表一种接口,目的是为各种不同的数据结构提供统一的访问机制。

任何数据结构,只要部署了 Iterator 接口,就可以完成遍历操作!(当然要是可遍历的数据结构,像 let、Number 这种不可遍历的自然就不支持迭代器了)

首先来了解三个相关的类型:

  • 可迭代对象 — 类似Array、NodeList等数据结构,当然我们也可以自己定义一个可迭代对象
  • 迭代器对象 — 是指任何具备next()方法的,且方法返回迭代结果对象的对象,其可以用于执行迭代。
  • 迭代器结果对象 — 指由next()方法返回的,具有属性valuedone的对象,用于保存每次迭代的结果。

具体来说,当我们通过使用next()方法实现了一个符合迭代协议的对象时,这个对象就被称为迭代器对象。此时,next()方法会返回一个拥有属性valuedone的对象,即迭代器结果对象。对于迭代器结果对象中的两个属性:value对应的是迭代时的next值;而done则是用于标识是否已经迭代到序列的最后一个值。

1.1 迭代协议

至于迭代协议 Iterator protocol ,官方文档中的大致意思是:

首先,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。

迭代协议具体分为两个协议:可迭代协议迭代器协议

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为。而要成为一个可迭代对象,需要实现**@@iterator** 方法,即一个名为 [Symbol.iterator] 的方法,该方法没有参数,其返回值为一个符合迭代器协议的对象。

当一个对象要被迭代时(例如被放入一个 for of 循环中时),会自动地调用它的**@@iterator** 方法,然后通过该方法返回的迭代器对象获取要迭代的值。

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。简单来说,其定义了一个迭代器对象的实现标准,即:

  1. 该对象必须实现了一个**next()** 方法
  2. 该对象中所有迭代器协议的方法(next()return()throw())都应返回实现 IteratorResult 接口的对象,即拥有属性valuedone的对象

至于更详细的内容,可以参阅官方文档。

1.2 哪些数据结构支持迭代器?

支持 Iterator 接口的数据结构有:

  1. String
  2. Array
  3. Map
  4. Set
  5. TypedArray
  6. 函数中的 arguments 对象
  7. DOM 中的 NodeList 对象

另外,需要注意的是,实际上我们也可以不使用这些数据结构,而是自己定义一个符合上面迭代协议的对象!

1.3 迭代器原理

根据上面的内容,我们可以发现,当我们迭代一个可迭代对象时,实际上是通过该对象身上的**@@iterator** 方法获得一个迭代器对象,然后重复地调用该迭代器对象中的**next()** 方法,而该方法会返回一个迭代器结果对象,其中包含了下一次迭代的值,以及一个标识是否结束的布尔值。重复操作直到该布尔值为 true 时,迭代就停止了。

1.4 迭代器的基本应用

对于可迭代对象,我们实际上并不用显示地调用**@@iterator** 方法,而是可以通过 for of、扩展操作符等直接操作!

// 通过 for of 操作可迭代对象
const Iterable = ['1', '2', '3', '234', '555'];
for (let v of Iterable) {
    console.log(v)	// '1', '2', '3', '234', '555'
}

// 通过扩展操作符操作可迭代对象
data = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(Math.max(...data)); 	// 8

另外,迭代器也可以用于 ES6 中的解构赋值:

let arr = [10 , 18 , 20];
let [a , b , c] = arr;
console.log(c);		// 20
1.5 自定义的可迭代对象

根据前面的内容,要实现一个可迭代对象,首先需要实现**@@iterator** 方法,其需要返回一个内置了**next()** 方法的迭代器对象。next() 方法需要能够实现迭代,并返回一个拥有属性valuedone的迭代器结果对象。

因此,实现如下:

const myIterable = {
  name: 'my iterable obj',
  data: ['test1', 'iterable', '222', 'test2', 'ababab'],
  // 迭代器方法
  [Symbol.iterator]() {
    // 索引
    let index = 0;
    // 返回一个迭代器对象
    return {
      // next()方法
      next: () => {
        if (index < this.data.length) {
          const res = {
            value: this.data[index],
            done: false,
          };
          index++;
          return res;
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  },
};

// 通过 for of 遍历该可迭代对象
for (let v of myIterable) {
    console.log(v)	// 'test1', 'iterable', '222', 'test2', 'ababab'
}

2.生成器

2.1 生成器的基本概念和基本原理

生成器,在官方文档中的描述是这样的:

生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。

生成器同时也是一种异步编程解决方案。

生成器函数使用**function***语法编写。最初调用时,生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。

而通过调用生成器的**next()**方法消耗值时,生成器函数才会执行,并一直运行直到遇到 yield 关键字。即,第一次调用next()方法时,生成器函数会一直执行到其代码中的第一个 yield 关键字处;第二次调用next()方法时,生成器函数会从第一个 yield 关键字处执行到第二个 yield 关键字处。

下面是一个例子:

// 定义一个生成器函数
function* fn() {
    console.log('生成器函数第一次执行');
    yield 'result1';
    console.log('生成器函数第二次执行');
    yield 'result2';
    console.log('生成器函数第三次执行');
    yield 'result3';
    console.log('生成器函数第四次执行');
}

// 调用生成器函数,得到一个生成器对象
let gen = fn()	// 此处生成器函数内部的代码并不会执行!仅仅是得到了一个生成器对象

// 调用生成器对象的next()方法
console.log(gen.next())	// 生成器函数第一次执行 {value: 'result1', done: false}
console.log(gen.next())	// 生成器函数第二次执行 {value: 'result2', done: false}
console.log(gen.next())	// 生成器函数第三次执行 {value: 'result3', done: false}
console.log(gen.next())	// 生成器函数第四次执行 {value: undefined, done: true}

可以看到,第一次调用next()方法时,生成器函数内部的代码仅执行到第一个 yield 关键字处,并将 yield 关键字后表达式的值作为next()方法所返回的迭代器结果对象中value的值!

而第二次调用next()方法时,则是从第一个 yield 关键字后开始执行,直到遇到第二个 yield 关键字,并同样地将yield 关键字后表达式的值作为next()方法所返回的迭代器结果对象中value的值返回出去…重复此操作,直到最后一次调用next()方法,此时从最后一个 yield 关键字后开始执行,直到函数结束,且此时返回的值中,value的值为undefined,而done的值为true,说明迭代已经结束!

2.2 生成器函数中的yield

通过上面的例子,我们可以了解到,执行next()方法后,生成器函数会将某一个yield关键字后面的表达式的值作为next()方法的返回值,并将函数暂停在yield关键字处。

而上面的例子中没有提及的是,如果我们在调用next()方法时,传入参数,那么我们传入的参数会变成上一次暂停处的yield表达式的值!

下面通过一个例子来理解:

// 定义一个生成器函数
function* fn(str) {
    console.log(str); 
    let a = yield 1;  // 第一次调用暂停的位置
    console.log(a);   
    let b = yield 2;  // 第二次调用暂停的位置
    console.log(b);  
    let c = yield 3;  // 第三次调用暂停的位置
    console.log(c);
    return 100
}

// 获取生成器对象
let gen = fn('测试传参')

// 第一次调用next方法,传入一个参数 'a'
console.log(gen.next('a'))
// 此时是第一次调用,该参数会被丢弃,因为没有上一次暂停处!
// 执行结果为:测试传参 {value: 1, done: false}

// 第二次调用,传入一个参数 'b'
console.log(gen.next('b'))
// 此时有上一次调用暂停的位置:let a = yield 1;
// 此时 'yield 1' 整个表达式的值被赋为我们传入的参数 'b'
// 因此本次的执行结果为:b {value: 2, done: false}

// 第三次调用,传入一个参数 'c'
console.log(gen.next('c'))
// 与上面同理,上一次暂停位置的yield表达式 'yield 2' 的值被赋为 'c'
// 所以本次执行结果为:c {value: 3, done: false}

// 最后一次调用,传入参数 'd'
console.log(gen.next('d'))
// 还是同理,'yield 3' 的值被赋为 'd',另外,生成器函数中有return,所以这里迭代器结果对象中的value有值了
// d {value: 100, done: false}

2.3 生成器的应用 — 模拟异步问题
function fa() {
    setTimeout(() => {
        let yon = '用户数据';
        //传入参数并调用
        han.next(yon);
    }, 1000);
}

function fb() {
    setTimeout(() => {
        let din = '订单数据';
        han.next(din);
    }, 1000);
}

function fc() {
    setTimeout(() => {
        let shang = '商品数据';
        han.next(shang);
    }, 1000);
}

function* fn() {
    let a = yield fa();
    console.log(a);        //1s 用户数据
    let b = yield fb();
    console.log(b);        //2s 订单数据
    let c = yield fc();
    console.log(c);        //3s 商品数据
}
let han = fn();
//调用
han.next();

总的来说,生成器是ES6中提供的一种异步编程的解决方法,但通过上面的例子可以看到,单纯地利用生成器来实现异步操作,会导致代码可读性比较低,在实际开发中使用更多的还是 async/await 和 promise 。

你可能感兴趣的:(JS进阶,javascript)