Iterator迭代器、Generator生成器

157 阅读7分钟

Iterator-迭代器

认识迭代器

迭代器是帮助我们对某个数据结构进行遍历的对象

在JS中,迭代器也是一个具体的对象,但是这个对象需要符合迭代器协议(iterator protocol)

MDN:迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式

在JS中这个标准就是一个特定的 next 方法

next方法有如下要求:

next 是一个无参数或者有一个参数的函数,返回一个有以下两个属性的对象

  • done(boolean)

    如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)

  • value

    迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

总结:

迭代器是一个对象,符合迭代器协议

const iterator = { next: function() {return {}}}

// 数组
const names = ["aaa", "bbb", "ccc"]

// 创建一个迭代器对象来访问数组
let index = 0

const namesIterator = {
  next: function () {
    if (index < names.length) {
      return { done: false, value: names[index++] }
    } else {
      return { done: true, value: undefined }
    }
  },
}

console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
image.png

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

function createArrayIterator(arr) {
  let index = 0
  return {
    next: function () {
      if (index < arr.length) {
        return { done: false, value: arr[index++] }
      } else {
        return { done: true, value: undefined }
      }
    },
  }
}

const names = ["aaa", "bbb", "ccc"]
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())

const otherIterator = createArrayIterator([1, 3, 4, 5])

可迭代对象

认识可迭代对象

当一个对象实现了iterable protocol 协议时,它就是一个可迭代对象(注意跟迭代器协议不一样)

MDN中iterable protocol 协议

这个对象的要求是,必须实现@@iterator方法,在代码中我们使用 Symbol.iterator访问该属性

// 创建一个迭代器对象来访问数组
const iterableObj = {
  names: ['aaa', 'bbb', 'ccc'],
  [Symbol.iterator]: function() {
    let index = 0
    // 返回一个迭代器
    return {  
      next: () => {   // 为了拿到names,这里需要箭头函数,让this指向iterableObj
        if (index < this.names.length) {
          return { done: false, value: this.names[index++]}
        } else {
          return { done: true, value: undefined}
        }
      }
    }
  }
}

iterableObj 对象就是一个可迭代对象

console.log(iterableObj[Symbol.iterator]); // 获取到函数

// 调用函数返回一个迭代器
const iterator = iterableObj[Symbol.iterator]()
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

// 每次调用都返回一个新的迭代器对象
const iterator2 = iterableObj[Symbol.iterator]()
image.png

for...of 实际上就是一种语法糖

for...of 可以遍历的东西必须是一个可迭代对象

// for...of
const obj = {name: 'xxx', age: 18}
// for (const item of obj) {
//   console.log(item); // TypeError: obj is not iterable
// }

for(const item of iterableObj) {
  console.log(item); // aaa bbb ccc
}

实际上 for...of 就是在不断调用next,然后返回value,根据done来判断是否遍历结束

原生可迭代对象

事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象

例如:String,Array,Map,Set,arguments对象,NodeList集合

比如我们创建一个数组,打印[Symbol.iterator]属性,可以看到是有返回迭代器的函数的,所以也可以调用next()

const names = ['aaa', 'bbb', 'ccc']
console.log(names[Symbol.iterator]);
// ƒ values() { [native code] }

const iterator1 = names[Symbol.iterator]() // 返回迭代器对象
console.log(iterator1.next()); // 调用next
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());
image.png

其他也一样,比如Set

const set = new Set()
set.add(10)
set.add(100)
set.add(1000)
console.log(set[Symbol.iterator]); // ƒ values() { [native code] }
// 可以for...of遍历
for (const item of set) {
  console.log(item); // 10 100 1000
}

可迭代对象的应用

  • JavaScript中语法:for ...of、展开语法、yield*、解构赋值
  • 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])
  • 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)

展开语法

const iterableObj = {
  names: ["aaa", "bbb", "ccc"],
  [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 arr = ['ddd']
const newNames = [...arr, ...iterableObj]
console.log(newNames); // ['ddd', 'aaa', 'bbb', 'ccc']

// 对象使用展开语法
const obj = { name: "xxx", age: 18 }
const newObj = { ...obj }
console.log(newObj) // {name: 'xxx', age: 18}

注意:对象可以用展开语法,是ES9新增的特性,用的不是迭代器。是内部做了特殊的处理

解构语法

const titles = ["aaa", "bbb"]
const [t1, t2] = titles
console.log(t1, t2); // aaa bbb

对象的解构同样是ES9新增特性

创建一些对象时要求传入可迭代对象

Set

const set1 = new Set(iterableObj)
const set2 = new Set(titles)

Promise.all,Array.from

const arr1 = Array.from(iterableObj)

Promise.all(iterableObj).then(res => {
  console.log(res) // aaa bbb ccc
})

自定义类的迭代

我们定义一个类,希望通过这个类创建出来的对象是可迭代的

案例:创建一个 Person 类

class Person {
  constructor(name, age, friends) {
    this.name = name
    this.age = age
    this.friends = friends
  }

  // 新增friend
  entry(newFriend) {
    this.friends.push(newFriend)
  }

  // 定义 [Symbol.iterator] 
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.friends.length) {
          return { done: false, value: this.friends[index++] }
        } else {
          return { done: true, value: undefined}
        }
      }
    }
  }
}

const p1 = new Person('xxx', 18, ["aaa", "bbb"])
p1.entry("ccc")

for (const f of p1) {
  console.log(f); // aaa bbb ccc
}

Generator-生成器

前情提要

function foo() {
  const value1 = 100
  console.log(value1)

  const value2 = 200
  console.log(value2)

  const value3 = 300
  console.log(value3)
}

foo()

如果我们想要在打印完value1之后,暂停执行,注意是暂停!,也就是说可以恢复函数的执行的,因此我们不能直接return,return函数执行就终止了

认识生成器

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等

生成器函数与普通函数的区别:

  • 需要在 function 后面加一个符号:*

  • 生成器函数可以通过yield关键字来控制函数的执行流程

  • 返回值是一个生成器(Generator)

    事实上生成器是一种特殊的迭代器

定义一个生成器函数

function* foo() {
  console.log('函数开始执行');

  const value1 = 100
  console.log("value1: ", value1);
  yield // 暂停

  const value2 = 200
  console.log("value2: ", value2);
  yield // 暂停

  const value3 = 300
  console.log("value3: ", value3);
  yield // 暂停

  console.log('函数结束执行');
}

生成器函数的执行

调用生成器函数时, 会给我们返回一个生成器对象

// 直接foo() 函数并没有执行
const generator = foo()
console.log(generator); // foo {<suspended>}

如果我们想让函数执行,需要调用 next

// 开始执行第一段代码
generator.next() // 函数开始执行 value1: 100
// 执行第二段代码
generator.next() // value2: 200
// 执行第三段代码
generator.next() // value3: 300
generator.next() // 函数结束执行

我们知道迭代器的next是有返回值的,一个对象

const generator = foo()
console.log("返回值1: ", generator.next()); 
// 返回值1:  {value: undefined, done: false}

如果我们不希望next返回的是一个undefined,我们可以通过yield来返回结果

function* foo() {
  console.log('函数开始执行');

  const value1 = 100
  console.log("value1: ", value1);
  yield value1

  const value2 = 200
  console.log("value2: ", value2);
  yield value2

  const value3 = 300
  console.log("value3: ", value3);
  yield value3

  console.log('函数结束执行');
}
image.png

生成器传递参数

我们在调用next函数的时候,可以给它传递参数,这个参数会作为上一个yield语句的返回值

image.png image.png

return函数

让生成器提前结束,用的少

function* foo(num) {
  console.log('函数开始执行');

  const value1 = 100 * num
  console.log("value1: ", value1);
  const n = yield value1

  // 不会执行了
  const value2 = 200 * n
  console.log("value2: ", value2);
  yield value2

  const value3 = 300
  console.log("value3: ", value3); 
  yield value3 

  console.log('函数结束执行');
}

const generator = foo(5)
console.log("返回值1: ", generator.next());

// 第二段代码的执行, 使用了return
// 那么就意味着相当于在第一段代码的后面加上return, 就会提前终端生成器函数代码继续执行
console.log("返回值2: ", generator.return()); // 使用return
console.log("返回值3: ", generator.next()); 
image.png

throw函数

用的很少

在生成器函数内部抛出异常

function* foo() {
  console.log("代码开始执行")

  const value1 = 100
  try {   // 
    yield value1
  } catch (error) {
    console.log("捕获到异常情况:", error)
    yield "abc"
  }

  console.log("第二段代码继续执行")
  const value2 = 200
  yield value2

  console.log("代码执行结束")
}

const generator = foo()

const result = generator.next()
generator.throw("error message")

// 代码开始执行
// 捕获到异常情况: error message

生成器替代迭代器使用

生成器是一种特殊的迭代器

而且我们还可以使用yield* 来生成一个可迭代对象

// 生成器替代迭代器
function* createArrayIterator(arr) {
  // 1. 第一种写法
  // yield "aaa"
  // yield "bbb"
  // yield "ccc"

  // 2. 第二种写法
  // for (const item of arr) {
  //   yield item  
  // }

  // 3. 第三种写法(相当于是for ... of 的语法糖)
  yield* arr   // 后面跟的必须是可迭代对象
}

const names = ['aaa', 'ccc', 'ddd']
const namesIterator = createArrayIterator(names)
console.log(namesIterator.next()); // { value: 'aaa', done: false }
console.log(namesIterator.next()); // { value: 'bbb', done: false }
console.log(namesIterator.next()); // { value: 'ccc', done: false }
console.log(namesIterator.next()); // { value: undefined, done: true }

案例:创建一个函数,这个函数可以迭代一个范围内的数字

function* createRangeIterator(start, end) {
  let index = start
  while (index < end) { 
      // 不需要自己手动返回迭代器对象了(return带有next函数的对象)
    yield index++
  }
}

const rangeIterator = createRangeIterator(10, 20)
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
// { value: 10, done: false }
// { value: 11, done: false }
// { value: 12, done: false }

异步代码的处理方案

假如说现在有一个需求,我们需要向服务器发送网络请求,一共需要发送三次请求

对上一次请求到的结果做一个处理之后再发送一次新的请求

// 网络请求数据的函数
function requestData(url) {
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      // 拿到请求的结果
      resolve(url)
    }, 2000);
  })
}

1.第一种方案,多次回调

产生回调地狱

requestData("BASE").then(res => {
  // console.log(res); //拿这个结果处理后再发送请求
  requestData(res + 'aaa').then(res => {
    requestData(res + 'bbb').then(res => {
      console.log(res); // 最终拿到我们想要的结果
    })
  })
})

// 输出:BASEaaabbb

2.第二种方案:Promise中then的返回值来解决,then也返回一个Promise

requestData("BASE").then(res => {
  return requestData(res + 'aaa')
}).then(res => {
  return requestData(res + 'bbb')
}).then(res => {
  console.log(res);
})
// BASEaaabbb

代码阅读性很差

3.第三种方案:Promise + generator实现

// 先定义生成器函数
function* getData() {
  const res1 = yield requestData('BASE')
  const res2 = yield requestData(res1 + 'aaa')
  const res3 = yield requestData(res2 + 'bbb')
  console.log(res3);
}

执行上面函数的方式有三种:

  • (1)手动执行生成器函数
const generator = getData() // 先调用函数,返回生成器
// 通过next调用
generator.next().value.then(res => {
  generator.next(res).value.then(res => {
    generator.next(res).value.then(res => {
      generator.next(res)
    })
  })
})

generator.next() 返回结果对象{value: , next: }

并且这个value是promise

  • (2)自己封装了一个自动执行的函数

    使用递归

function execGenerator(genFn) {
  const generator = genFn() // 拿到生成器
  function exec(res) {
    const result = generator.next(res)
    if (result.done) { // 为true则说明执行结束
      return result.value
    }	
    result.value.then(res => {
      exec(res)  // 只要没有执行结束,就递归调用exec来接着执行
    })
  }
  exec()
}

execGenerator(getData)
  • (3)使用第三方包 co,自动执行生成器函数

npm i co

const co = require('co')
co(getData) // BASEaaabbb

在还没有async/await 之前,就是使用上面的几种方法

4.第四种方案:async/await

也就是我们目前使用使用最多的,在ES8新增的

实际上,就是 generator 实现的一种语法糖

*变成 async,yield -> await

async function getData() {
  const res1 = await requestData('BASE')
  const res2 = await requestData(res1 + 'aaa')
  const res3 = await requestData(res2 + 'bbb')
  console.log(res3);
}