详解js的迭代器和生成器

528 阅读5分钟

迭代器

什么是迭代器

通过使用迭代器我们可以不用在意所要遍历对象的内部结构而通过调用指定方法可以达到迭代的目的

什么是迭代

在学习迭代器之前,我们先简单的了解一下什么是迭代,用一句话来说迭代就是重复执行一段逻辑

迭代器执行逻辑

总的来说,迭代器就是可迭代对象对外提供的一个接口(Symbol.iterator方法),通过这个接口可以按顺序访问对象中的各个元素,可以通过调用迭代器对象的next方法
存在迭代方法的对象,称为可迭代对象,可迭代对象可以通过for...of循环调用,其原理就是for...of循环会找到迭代器方法,自动生成迭代对象并调用其next()方法。此外还可通过...操作符展开,以及Array.from()等方法转为数组

为什么要使用迭代器

通过使用迭代器可以按顺序访问对象中的元素内容,而又不需要关心对象的内部结构

js中可迭代对象有哪些

1.数组
2.字符串
3.Set对象
4.Map对象

迭代器原理

1.可迭代对象:具有专用迭代器方法(Symbol.iterator),且该方法返回迭代器对象的对象
2.迭代器对象:具有next()方法,且返回值为迭代结果对象
3.迭代结果对象:具有属性value和done的对象

迭代流程

1.调用其迭代器,获得迭代对象
2.重复调用迭代器对象的next()方法
3.直至返回done为true的迭代结果对象

**注:**在可迭代对象中,迭代器方法会返回自身,因为迭代对象本身就是可迭代的

注意:可迭代对象和迭代器有一个重要的特点,就是他们是惰性的,只有当真实用到下一个值时,才会去对其进行计算。举例:对字符串进行切分,如果使用split方法,即便只用一个单词,也需要处理整个字符串,而迭代器可以避免这种情况。

实现迭代器

/**
 * ep1:Range类
 * 通过Range方法实例化一个可迭代对象
 * */
class Range {
  constructor(from, to) {
    this.from = from
    this.to = to
  }
  // 迭代器方法
  [Symbol.iterator]() {
    let next = Math.ceil(this.from)
    let last = this.to
    // 迭代器对象返回迭代器结果
    return {
      next() {
        // 迭代结果对象
        return next <= last ? { value: next++ } : { done: true }
      },
    }
  }
}



/**
 * ep2:map方法
 * 将传入的itera经过f进行处理并将结果进行返回
 * */
function map(iteraObj, f) {
  let iterator = iteraObj[Symbol.iterator]()
  return {
    [Symbol.iterator]() {
      return this
    },
    next() {
      let i = iterator.next()
      if (i.done) {
        return i
      } else {
        return { value: f(i.value) }
      }
    },
  }
}

生成器

为什么要有生成器

迭代器这个概念固然是好的,但是他的创建和使用会让事情变得有些复杂,因此提出生成器概念,来简化自定义迭代器的创建。

什么是生成器

生成器的创建和函数类似,只是在函数名前面增加一个*号

生成器的创建流程

1.通过function*的方式创建一个生成器函数
2.调用生成器获取一个生成器对象(调用生成器函数并不会执行该函数,而是会返回一个生成器对象,该生成器对象本质是一个迭代器)
3.调用它的next()方法,会使生成器函数的函数体从当前位置开始执行,直到遇到一个yield语句
4.yield语句的值会成为调用迭代器的next()方法的返回值
注:不能使用箭头函数定义生成器函数

生成器使用

// 生成器
function* gen() {
  console.log('dd1')
  // yield可以让生成器停止和开始执行
  yield 'stop-1' //这里yield类似return,yield后的值会出现在next()的返回对象里
  console.log('dd2')
  return 'demo'
}

生成器需要配合yield使用,当然yield也只能在生成器函数中使用
在生成生成器对象后,通过调用next方法,开始执行代码,值得注意的是,当遇到return/yield时,停止代码执行,并将return/yield后面的值作为next方法的返回值中value属性的值,是的,next方法会返回一个迭代结果对象

function* gen() {
  console.log('dd1')
  console.log('stop-1', yield 'stop-1') 
  console.log('dd2')
  console.log('stop-2', yield 'stop-2')
  return 'demo'
}

let g = gen()

console.log(g.next())  //{value: 'stop-1', done: false}

yield和return是存在不同的,首先最明显的就是一个生成器函数内可以存在多个yield,而只能存在一个return,return会直接终端执行,除了这点外,还有一个很重要的区别,yield可以输入输出,yield会接收next方法的第一个参数,但是这里要注意一下,第一个yield接收的是第二个next方法的参数

// 生成器
function* gen() {
  console.log('dd1')
  console.log('stop-1', yield 'stop-1')   //2-2
  console.log('dd2')
  console.log('stop-2', yield 'stop-2')
  return 'demo'
}

let g = gen()

console.log('1=>', g.next())
console.log('2=>', g.next('2-2'))

生成器的常见用法

1.作为可迭代对象,通过for...of循环调用

function* nTimes(n) {
  while (n--) {
    yield
  }
}
for (let _ of nTimes(3)) {
  console.log('foo')
}
function fn() {
  return 'fn-data'
}

2.使用yield实现输入和输出

function* genFn(arg) {
  console.log(arg)
  // console.log(yield, '<=first')
  let data = yield fn()
  console.log(data)
  console.log(yield)
}
let gf = genFn('arg1')
// 第一次调用next()传入的值不会被使用
let f = gf.next().value
gf.next(f)

3.产生可迭代对象
这里在yield后面生成了一个可迭代对象,因此可以直接在for...of中使用

// 3.产生可迭代对象
// 普通写法
function* genIte() {
  for (const x of [1, 2, 3]) {
    yield x
  }
}
// 增强写法
function* genItePlus() {
  yield* [1, 2, 3]
}
for (let i of genItePlue()) {
  console.log(i)
}

生成器作为默认迭代器

class Demo {
  constructor() {
    this.arr = [1, 2, 3]
  }
  *[Symbol.iterator]() {
    yield* this.arr
  }
}

提前终止生成器

可以调用生成器对象上的return方法和throw方法,来提前终止对象