ES6 Iterator和Generator

Iterator

Iterator迭代器想必在很多语言中都有实现,JavaScript也在ES6中引入了迭代器,而且在很多语法特性中得以应用,例如,for...of循环,展开运算符(...),Generator生成器。

现在很多数据接口中部署有迭代器,并且可以通过for...of进行,例如数组

let colors = ['red', 'green', 'yellow']
for (let value of colors ) {
   console.log(value)
} 
red
green
yellow 

再比如新的Map和Set数据结构

let family = new Map()
family.set('father', 'mick')
family.set('mother', 'lily')
family.set('son', 'jeff')

for (let member of family) {
 console.log(member)
} 
[ 'father', 'mick' ]
[ 'mother', 'lily' ]
[ 'son', 'jeff' ] 
let colorSet = new Set(colors)

for (let color of colorSet) {
 console.log(color)
} 
red
green
yellow 

上述例子我们都通过for...of语法来完成迭代,其实实际的迭代器部署在每个数据对象的Symbol.iterator属性中

以数组为例

const iterator = colors[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next()) 
{ value: 'red', done: false }
{ value: 'green', done: false }
{ value: 'yellow', done: false }
{ value: undefined, done: true } 

还有很多内建的迭代器

const mapIterator = family[Symbol.iterator]()
console.log(mapIterator.next())
console.log(mapIterator.next())
console.log(mapIterator.next())
console.log(mapIterator.next()) 
{ value: [ 'father', 'mick' ], done: false }
{ value: [ 'mother', 'lily' ], done: false }
{ value: [ 'son', 'jeff' ], done: false }
{ value: undefined, done: true } 
let colorSet = new Set(colors)
const setInterator = colorSet[Symbol.iterator]()
console.log(setInterator.next())
console.log(setInterator.next())
console.log(setInterator.next())
console.log(setInterator.next()) 
{ value: 'red', done: false }
{ value: 'green', done: false }
{ value: 'yellow', done: false }
{ value: undefined, done: true } 

不过像Map和Set这样的数据结构是有对外的方法

Map的entries方法

family[Symbol.iterator] === family.entries // true 

Set的values方法

colorSet[Symbol.iterator] === colorSet.values // true 

上述例子可以看到迭代器其实是个特殊的对象,每次调用其next方法返回一个结果对象,结果有两个属性,一个是value,一个是done。在没有迭代完之前done的值为false,执行完done为true。

我们用TypeScript可以用接口的表示形式将其规格描述出来

interface Iterable {
 [Symbol.iterator]() : Iterator,
}

interface Iterator {
 next(value?: any) : IterationResult,
}

interface IterationResult {
 value: any,
 done: boolean,
} 

而for...of则会使用对象的默认迭代器方法进行迭代,其实我们可以为自定义对象部署迭代器,这样for...of也可以对我们的对象进行遍历,举个简单的例子

const obj = {
 '0': 'item1',
 '1': 'item2',
 '2': 'item3',
 'length': 3,
 [Symbol.iterator]: [][Symbol.iterator]
}

for (let item of obj) {
 console.log(item)
} 
item1
item2
item3 

这里我们借用了数组的迭代器(数组的默认迭代器也是values只是没有提供外部API)

那么我们可以自己创建一个迭代器方法供对象使用吗?

当然可以!

const iterObj = {
 'parent': 'jeff',
 [Symbol.iterator]: function iterator () {
   const keys = Object.keys(this)
   let idx = 0
   return {
     next: function () {
       const value = keys[idx]
       idx += 1
       const done = value === undefined ? true : false
       return {
         value,
         done
       }
     }
   }
 }
}
for (let key of iterObj) {
 console.log(key)
} 
parent 

这里我们实现了对象的默认迭代方法,其实就是keys方法,当然我们可以实现其他的一些迭代。

Generator

Generator函数是ES6引入的异步编程解决方案,本身也是一个可迭代的对象(Iterable),执行后返回迭代器(Iterator)

function * iterable () {
   yield 1;
   yield 2;
   yield 3;
} 
const iterator = iterable()
iterator[Symbol.iterator] instanceof Function // true 
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: undefined, done: true } 

在function和函数名间放个*就编程了生成器了,内部通过yield关键字实现函数暂停

我们来个复杂的例子

function * compute (x) {
 let y = yield x + 1
 let z = yield y * 2
 return z
}

const stepCompute = compute(1)
const log = console.log
log(stepCompute.next())
log(stepCompute.next())
log(stepCompute.next()) 
{ value: 2, done: false }
{ value: NaN, done: false }
{ value: undefined, done: true } 

这里可能大家有些疑惑好像value值并没有按照我们的预期运行,我们期待的可能是

{ value: 2, done: false }
{ value: 4, done: false }
{ value: 4, done: true } 

原因是因为yield表达式的返回和next()调用返回的结果不是一回事儿,函数中的yield表达式返回值必须由下一次next调用传入参数来指定,所以要实现上述效果,我们需要这样

function * compute (x) {
 let y = yield x + 1
 let z = yield y * 2
 return z
}

const stepCompute = compute(1)
const log = console.log
let lastValue = stepCompute.next()

log(lastValue)

lastValue = stepCompute.next(lastValue.value)
log(lastValue)

lastValue = stepCompute.next(lastValue.value)

log(lastValue) 
{ value: 2, done: false }
{ value: 4, done: false }
{ value: 4, done: true } 

虽然实现了效果,但是这个操作怎么这么挫,我们是不是可以实现一个函数来循环执行这些操作?

function * compute (x) {
 let y = yield x + 1
 console.log(y)
 let z = yield y * 2
 console.log(z)
 return z
}

function run (gen) {
 return function () {
   const iterator = gen(...arguments)
   let lastResult = iterator.next()
   while (!lastResult.done) {
     lastResult = iterator.next(lastResult.value)
   }
   return lastResult.value
 }
}

console.log(run(compute)(1)) 
2
4
4 

这样很棒不是吗?我们来个比较实际的例子,比如nodejs中的读取文件。

呃,显然我们的run函数不能支持这么复杂的操作,我们来改写一下

function run (gen) {
 let lastResult
 let isStop = false
 let iterator
 function runTask () {
   while (!lastResult.done && !isStop) {
     if (typeof lastResult.value === 'function') {
       lastResult.value(function (err, data) {
         if (err) {
           iterator.throw(err)
           return
         }
         lastResult = iterator.next(data)
         isStop = false
         runTask()
       })
       isStop = true
     } else {
       lastResult = iterator.next(lastResult.value)
     }
   }
 }

 return function () {
   iterator = gen(...arguments)
   lastResult = iterator.next()
   runTask()
 }
}


function readFile (path) {
 return function (callback) {
   fs.readFile(path, callback)
 }
}


run(function * (filepath) {
 let data
 try {
   data = yield readFile(filepath) 
 } catch (error) {
   console.error(error)
 }
 if (data) {
   console.log(data.toString())
 }
})('./test.txt') 

这里主要是定义一个runTask函数让while循环可控,当然你也可以通过递归来替换while循环。

另外还添加了错误处理功能,iterator.throw可以抛出错误,然后我们就可以在生成器函数中用try catch处理错误

我们还以在生成器函数内部调用另外的生成器函数

function * gen1 () {
   yield 1;
   yield 2;
}

function * gen2 () {
   yield 'hello';
   yield 'world';
}

function * gen () {
   yield *gen1();
   yield *gen2();
} 

到此,对于生成器应该大致了解了

下一步我们就可以看看async和await

参考

  • ECMAScript 6 入门
  • 深入理解ES6