迭代器和生成器在前端业务里经常有用到,但是可能感受不太明显。特别是生成器,在react
中如果你有用过redux
中间件redux-saga
那么你一定对生成器很熟悉。
本文是笔者对于迭代器与生成器的理解,希望在项目中有所帮助.
在阅读本文之前,主要会从以下几点去探讨迭代器/生成器
- 迭代器是什么,想想为什么会有迭代器
- 生成器又是什么,它解决了什么样的问题
- 以实际例子阐述迭代器与生成器
正文开始...
迭代器是什么
参考mdn
上解释,迭代器是一个对象,每次调用next
方法返回一个{done: false, value: ''}
,每次调用next
返回当前值,直至最后一次调用时返回
{value:undefined,done: true}
时结束,无论后面调用next
方法都只会返回{value: undefined,done:true}
在过往的业务中,你一定用过for ... of
循环过数组
或者Map
const arr = [ { name: 'Maic', age: 18 }, { name: 'Tom', age: 10 } ] for (let item of arr) { console.log(item); /* {name: 'Maic', age:18},{name: 'Maic', age:18} */ }
因为数组就是可支持迭代器对象,并且for...of
可以中断循环,关于循环中断可以参考以前写的一篇文章你不知道的JS循环中断
因为数组是支持可迭代的对象,如果使用迭代器获取每组数据应该怎么做呢?
const arr = [ { name: 'Maic', age: 18 }, { name: 'Tom', age: 10 } ] const iterator = arr[Symbol.iterator](); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next());
我们执行node index.js
可以看到运行结果
当我们每次调用iterator.next()
值时,都会返回当前的值,并且返回的值是{value: xxx, done: false}
,直至最后,返回的值{value: undefined, done: true }
不知道你发现没有,上面迭代器,我是通过数组访问Symbol.iterator
方法,再调用返回的next
方法,最后得到当前的值
我们可以在控制台看下
数组是有这个Symbol.iterator
属性的
从以上迭代器特征中,我们可以得知,数组是通过一个Symbol.iterator
方法,返回一个next
方法,并且next
方法返回{value: xx, done: false}
,我们模拟一个迭代器
模拟迭代器
const iteratorObj = { value: [1, 2, 3], count:-1, next() { this.count++ return { value: this.value[this.count], done: !this.value[this.count] } } } console.log(iteratorObj.next()); console.log(iteratorObj.next()); console.log(iteratorObj.next()); console.log(iteratorObj.next());
打印的结果依次是:
{ value: 1, done: false } { value: 2, done: false } { value: 3, done: false } { value: undefined, done: true }
此时你会发现iteratorObj
就基本实现了一个迭代器的基本功能。
我们用一个对象模拟了迭代器,但是我们发现这个对象迭代器貌似没法复用
因此我们创建一个迭代器的工具函数
function createIteror(arr = []) { let count = -1; return { next: function () { count++ return { value: arr[count], done: count >= arr.length } } } } const newCreateInteror = createIteror([1, 2, 3]); console.log(newCreateInteror.next()); console.log(newCreateInteror.next()); console.log(newCreateInteror.next()); console.log(newCreateInteror.next());
结果是:
{ value: 1, done: false } { value: 2, done: false } { value: 3, done: false } { value: undefined, done: false }
因此createIteror
这个方法就具备了迭代器的功能
我们在这之前用iteratorObj
模拟了一个具备迭代器
的功能,但是如何让真正的对象支持迭代呢
让对象支持迭代器功能
不知道你发现没有,其实数组原型上是有Symbol.iterator
,所以如果要让一个对象支持迭代器功能,那么只需要遵循迭代协议
即可
const coustomerInteror = { value: [1, 2, 3], // 让对象支持迭代器协议,需要增加一个Symbol.iterator可访问的方法,并返回一个迭代器对象,迭代器对象可以调用`next`方法,在next方法中返回一个当前对象的值 [Symbol.iterator]: function () { let count = -1; return { next: () => { count++; return { value: this.value[count], done: count >= this.value.length } } } } } const newInter = coustomerInteror[Symbol.iterator](); console.log(newInter.next()); console.log(newInter.next()); console.log(newInter.next()); console.log(newInter.next()); for (let item of coustomerInteror) { console.log(item, '=result') }
可以看到打印的结果
因此让一个对象支持迭代器功能,只需要新增一个Symbol.iterator
方法,遵循迭代器
原则
支持所有对象可迭代
我们从以上结果得知要想一个对象支持迭代器功能,必须要有Symbol.iterator
这样的迭代器协议
因此我们可以在Object
原型上新增这样的一个迭代器协议
// 在Object.prototype原型上扩展Symbol.iterator Object.prototype[Symbol.iterator] = function () { let count = -1; return { next: () => { count++; const keys = Object.keys(this); return { value: this[keys[count]], done: count >= keys.length } } } } const cobj = { a: 1, b: 2 }; const iteror = cobj[Symbol.iterator](); console.log(iteror.next()); console.log(iteror.next()) console.log(iteror.next()) for (let item of cobj) { console.log(item, '=rs') } const [a, b] = cobj; console.log(a,b);
执行的结果是:
{ value: 1, done: false } { value: 2, done: false } { value: undefined, done: true } 1 =rs 2 =rs 1 2
你会发现当我们使用数组解构时,居然可以解构对象的值
const [a, b] = cobj; console.log(a,b);
本质上就是我们迭代器会自动调用iteror.next().value
然后一一赋值返回了。
所以支持迭代器
对象不仅可以for...of
也可以被数组解构
,这样所有var obj = {}
这样类似申明的对象都可以支持迭代器了。
构造函数支持可迭代
我们现在有个需求,需要支持通过构造函数new
出来的对象支持可迭代器功能
具体我们看下代码
class Person { constructor() { this.name = 'Maic'; this.age = 18; } [Symbol.iterator]() { let count = -1; return { next: () => { count++; // 获取对象的所有属性key const keys = Object.keys(this); return { value: this[keys[count]], done: count >= keys.length } } } } } const person = new Person(); const iter = person[Symbol.iterator](); console.log(iter.next(), '=='); console.log(iter.next(), '=='); console.log(iter.next(), '=='); for (let item of person) { if (item === 'Maic') { break; // 可以中断循环 } console.log(item) // 这里并不会打印 } const [name, age] = person; console.log(name, age)
本质上也是在构造函数Person
内部新增了Symbol.iterator
方法,并且返回了一个迭代器对象
打印的结果如下:
{ value: 'Maic', done: false } == { value: 18, done: false } == { value: undefined, done: true } == Maic 18
至此你应该非常了解迭代器的对象的特性了哈
能够for...of
循环中断,且能够数组解构
、扩展
,所以你知道为啥会有迭代器了吗?
那些原生API支持迭代器
首先是数组Array
,Map
,Set
只要是有迭代器特性,那么就可以被for...of
,数组解构等
生成器
这是es6新增的,参考generator[1]解释,生成器是一种异步解决的方案,也可以理解一种函数内部的状态机,能中断函数,也就是说能够控制函数的运行
具体我们以一个实际例子看下生成器是什么
function* genter() { yield 1; yield 2; } const gen = genter(); console.log(gen);
我们定义了一个普通函数,但是这个普通函数比较特殊,前面有*
,这就是定义生成器函数,我们暂定把gen
这个称呼为生成器对象
然后我们打印生成器对象,实际是就像函数调用一样,不过此时返回的是一个Object Generator
Object [Generator] {}
但是我们继续看下
function* genter() { yield 1; yield 2; } const gen = genter(); console.log(gen.next()); console.log(gen.next()); console.log(gen.next()); for (let item of gen) { console.log(item); }
此时打印的结果是
{ value: 1, done: false } { value: 2, done: false } { value: undefined, done: true }
我们看下生成器函数内部是有yield
这样的关键字
实际上这就是内部函数的状态机
,当你使用用生成器时,你调用next
就会返回一个对象,并且像迭代器一样返回{value: xxx, done: false}
因此在使用上,我们必须认清,生成器必须是带有*
定义的函数,内部是yield
执行的状态机
当我们调用函数生成器时,并不会立即执行,返回一个遍历对象并返回一个next
方法,当遍历对象调用next
时,就会返回yield
执行的状态机,并返回一个迭代器对象的值,yield
会在当前状态暂停,只有调用next
时,就会执行yield
,yield
value
表示当前yield
的值,done:false
表示当前遍厉没有结束,如果继续执行gen.next()
那么就会返回当前的yield
值,直到done:true
时,表示当前遍历对象已经完全遍历完毕。
我们再来看下这段代码
function* start() { console.log('start') } const genterStart = start();
此时你会发现并不会调用start
方法
但是你执行下面代码时,才会立即调用
function* start() { console.log('start') } const genterStart = start(); setTimeout(() => { genterStart.next(); }, 1000)
我们会发现定时定时1s妙后才执行start
方法,而且是通过next
去执行的。
所以此时这个start
变成了一个暂缓的执行函数
,同时我们要注意yield
只能用在*
定义的生成器内部
生成器-扁平化数组
我们在以往的业务中多少有写过扁平化数组,通常也是用递归实现多维数组的打平,现在我们尝试用生成器来实现一个扁平化数组
function* flat(arr) { for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (Array.isArray(item)) { // 如果是数组,则递归 yield* flat(item) } else { yield item } } } const sourceArr = [1, [[2, 3], 4], [5, 6]] const result = []; for (let item of flat(sourceArr)) { result.push(item) } console.log(result)// [1,2,3,4,5,6]
但是这个flat
貌似不太通用,因此可不可以像原生flat
方法一样,因此我们向下面这样做,在Array
的原型上新增一个方法,让所有的数组都能访问这个自定义方法
// Array的prototype中绑定一个公有方法 Array.prototype.$myFlat = function () { // 定义一个flat生成器 function* flat(arr) { for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (Array.isArray(item)) { // 递归当前flat yield* flat(item); } else { yield item } } } const ngen = flat(this); return [...ngen]; } const sourceArr2 = [1, 2, [3, 4, 5, 6, [7, 8]]] console.log(sourceArr2.$myFlat())
因此$myFlat
这个方法就像原生flat
一样了
生成器与迭代器的关系
当我们看到用*
定义的方法,就变成一个生成器,此时我们调用这个生成器方法,那么此时就可以for...of
循环了
function* test() { yield 1; yield 2; yield 3; } const gtest = test(); // gtest.next() { value:1,done: false} // for (let item of gtest) { // console.log(item) 这里相当于已经调用了gtest.next().value // } const [a, b, c, d] = gtest; console.log('abc', a, b, c, d)
打印的结果就是:
abc 1 2 3 undefined
我们进一步测试一下:
function* test() { yield 1; yield 2; yield 3; } const gtest = test(); console.log(gtest[Symbol.iterator]() === gtest) // true
这里我们就会发现gtest
可以通过Symbol.iterator
这个方法直接调用,居然于它本身相等。
从控制台中我们可以知道gtest
返回就是一个生成器对象,它的构造函数是GeneratorFunction
,并且原型上有Symbol.iterator
,而且是一个迭代器。
当你使用
... gtest[Symbol.iterator]().next(); gtest[Symbol.iterator]().next() gtest[Symbol.iterator]().next() // 以上等价于 /* gtest.next(); gtest.next(); gtest.next(); */
可以看下控制台打印的结果就知道了
所以大概了解生成器与迭代器的关系了么?本质上是通过生成器对象的prototype的Symbol.iterator
连接了起来
生成器函数的return
当我们在生成器函数内部return时,那么当调用next
迭代完所有的值时,继续调用next
,则会返回return
的值
什么意思,我们看下下面这段代码
function* test() { yield 1; yield 2; yield 3; return 4; } const gtest = test(); console.log(gtest.next());// {value: 1,done: false} console.log(gtest.next()); // {value: 2,done: false} console.log(gtest.next()); // {value: 3,done: false} console.log(gtest.next()); // {value: 4,done: true} console.log(gtest.next()); // {value: undefined,done: true}
yield
后面可以是变量或者具体函数返回值 你可以这么写
function* test() { let b = 2; const logNum = (num) => num yield 1; yield b; yield logNum(5); return 4; } const gtest = test(); console.log(gtest.next()); console.log(gtest.next()); console.log(gtest.next()); console.log(gtest.next());
执行结果如下
{ value: 1, done: false } { value: 2, done: false } { value: 5, done: false } { value: 4, done: true } { value: undefined, done: true }
生成器传参数
function* test() { let b = 2; const logNum = num => num const n = yield 1; // n为下面第二个yield(10)这里n = 10 yield n * b; // 这个n就是第二个next传入的,会把第一个yield当返回值,传给下个yield yield logNum(5); return 4; } const gtest = test(); console.log(gtest.next()); console.log(gtest.next(10)); // 20 console.log(gtest.next()); console.log(gtest.next()); /* { value: 1, done: false } { value: 20, done: false } { value: 5, done: false } { value: 4, done: true } { value: undefined, done: true } */
生成器捕获异常
主要是在yield
捕获异常,具体看下下面这个简单的例子
function* test() { try { yield 1; } catch (error) { console.log(error) } try { yield 2; } catch (error) { console.log(error, '---2'); } } const gen = test(); console.log(gen.next()) gen.throw('错误了'); console.log(gen.next()) // 并不会运行
当我们执行gen.next()
时会执行yield 1
此时返回{value: 1, done: false}
当我们执行gen.throw
时,此时yield 2
会暂停,并且就会中断了。并且后面的gen.next()
就是默认返回{value: undefined, done: true}
yield状态机
我们在这之前都见过yield
只能在生成器中使用,那到底有哪些使用,我们写个例子熟悉一下
function* a() { yield 1; yield 2; } function* b() { yield* a(); yield 3; } const bGen = b(); // console.log([...bGen]); [1,2,3] // console.log(bGen.next()) 注意这个与上面不能同时使用,不然这个bGen就是返回{value: undefined, done: true}
yield
后面能是函数返回值,能是变量,也可以是一个生成器函数
让一个对象的方法支持生成器
const obj = { * getName() { yield 'Maic' } } const person = obj.getName(); console.log(person.next()); // {value: 'Maic', done: false}
等价
const obj = { getAge: function *() { yield 18 } } const age = obj.getAge(); console.log(age.next()); // {value: 18, done: false}
生成器不能为new
function* a() { yield 1; yield 2; } // new a() error
生成器异步操作
在以往业务中肯定有这种场景,点击页面首先加载loading
,然后请求数据,当数据请求成功后,就结束loading
,我们看一段简单的伪代码
// 定义了一个获取数据的生成器方法,setTimeout模拟异步请求 function* getList() { yield new Promise((resolve, reject) => { setTimeout(() => { resolve({ code: 0, data: [ { name: 'Maic', age: 18 }, { name: 'Web技术学苑', age: 20 } ] }) }, 1000) }) }
然后我们定义一个loadUI
生成器
function* loadUI() { console.log('正在加载中...,开启loading...'); yield* getList(); console.log('加载完成,关闭loading') } const loadStart = loadUI(); // 加载数据,调用next().value 获取yield的值 const currentData = loadStart.next().value; currentData.then((res) => { if (res) { console.log(res); } loadStart.next(); // 关闭加载,加载完成,执行yield 后面的代码 });
或者你可以这样
function* loadUI() { console.log('正在加载中...,开启loading...'); const { data } = yield getList(); console.log(data); console.log('加载完成,关闭loading') } const loadStart = loadUI(); function getList() { const mockData = { code: 0, data: [ { name: 'Maic', age: 18 }, { name: 'Web技术学苑', age: 20 } ] }; setTimeout(() => { loadStart.next(mockData);// next传入数据当成yield状态机的返回值 }, 1000) } // 继续执行yield后面的代码 loadStart.next();
运行的结果依旧是一样的,这样我就可以通过loadStart
精准的控制数据请求在哪里执行了。如果我最后一行代码不执行,那么久不会执行后面的打印代码了,从而达到精准的控制函数内部的执行。
控制多个函数按顺序执行
假设有一个场景,就是fn2
依赖fn1
的结果而决定是否是否执行,fn3
依赖fn2
的状态是否继续执行,那怎么设计呢?生成器可以帮我们解决这个需求问题
function fn1() { return { code: 1, message: '我是fn1,你成功了,请进行下一步' } } function fn2() { return { code: 0, message: '我是fn2,失败了' } } function fn3() { console.log('恭喜你,闯关成功了...'); } const source = [fn1, fn2, fn3]; function* main(arr = []) { for (let i = 0; i < arr.length; i++) { yield arr[i]( "i") } } const it = main(source); for (let item of it) { console.log(item) if (item.code === 0) { break; } }
结果是:
{ code: 1, message: '我是fn1,你成功了,请进行下一步' } { code: 0, message: '我是fn2,失败了' }
当fn2
返回code:0
就会终止break
中止,当fn2
中返回的code
是1时,才会进入下一个迭代
当我们for...of
时,内部会依次调用next
方法进行遍历数据。因为是迭代器,每次next
的值返回的就是yield
的值,并且返回{value: xxx, done: false}
,直到最后{value: undefined, done: true}
总结
- 迭代器是一个对象,迭代器对象有一个
next
方法,当我们调用next
方法时,会返回一个对象{value: xx, done: false}
,value
就是当前迭代器迭代的具体值,当迭代器对象每调用一次next
方法时,就会获取当前的值,直到迭代完全,最后返回{done: true, value: undefined}
- 每一个迭代器都可以被
for...of
、数组解构
以及数组扩展 - 生成器函数,
yield
可以中断函数,当我们调用函数生成器时,实际上并不会立即执行生成器函数,当这个调用的函数生成器在调用时会返回一个迭代器,每次调用next
方法会返回一个对象,这个对象的值跟迭代器一样,并且返回的value
是yield
的值,每次调用,才会执行yield,后面的代码会中断。只有继续调用next
才会继续往后执行。 - 生成器函数调用返回的是一个迭代器,具备迭代器所有特性,
yield
这个状态机只能在生成器函数内部使用 - 以实际例子对
对象
扩展支持迭代器特性,如果需要支持迭代器特征,那么必须原型上扩展Symbol.iterator
方法,以$myflat
在数组原型上利用函数生成器实现扁平化数组等。 - 本文示例code-example[2]