迭代器和生成器

1.迭代器

1.1 什么是迭代器?

迭代器:本质上是一个对象,符合迭代器协议。

迭代器协议:

1.其对象要返回一个next函数

2.调用next函数返回一个对象,这个对象包含两个属性

2.1 done(完成),值为Boolean,返回true/false

2.1.1 如果这个迭代器没有迭代完成,返回{done:false}

2.1.2 如果这个迭代器迭代完成,返回{done:true}

2.2 val(当前值),可以返回js中的任意值

1.2 迭代器的基本实现

根据迭代器的定义,我们需要实现next函数并根据需要返回{done:xxx,val:xxx}

js 复制代码
// 定义索引
let index = 0
// 定义原始迭代对象
const arr = ['Alice', 'Bob', 'John']
// 定义一个迭代器对象
let iterator = {
    // next函数
    next() {
        // - 没有迭代完成,返回done:false,继续迭代
        if (index < arr.length) {
            return { done: false, value: arr[index++] }
        } else {
            // -- 迭代完成,done:true 
            return { done: true, value: undefined }
        }
    }
}

console.log(iterator.next())//{ done: false, value: 'Alice' }
console.log(iterator.next())//{ done: false, value: 'Bob' }
console.log(iterator.next())//{ done: false, value: 'John' }
console.log(iterator.next())//{ done: true, value: undefined }

说明1:迭代器是一个对象,实现next方法,next方法返回一个新的对象,对象中done用于观察迭代是否完成,对象中的值用于表示迭代当前值。既有next又有done和val,符合迭代器协议。

说明2:此时定义的一些变量如index、arr都暴露到了全局,违背了高内聚的开发思想,导致当前迭代器效率特别低,综上对目标迭代器进行封装。

1.3 迭代器的封装实现

js 复制代码
function createIterator(iterator) {
    let index = 0
    let _iterator = {
        next() {
            if (index < iterator.length) {
                return { done: false, value: iterator[index++] }
            } else {
                return { done: true, value: undefined }
            }
        }
    }
    return _iterator
}
let iter = createIterator(arr)
console.log(iter.next())//{ done: false, value: 'Alice' }
console.log(iter.next())//{ done: false, value: 'Bob' }
console.log(iter.next())//{ done: false, value: 'John' }
console.log(iter.next())//{ done: true, value: undefined }

2.可迭代对象

2.1 什么是可迭代对象

首先需要明确,迭代器对象和可迭代对象是完全不同的东西,尽管他们之间会存在联系,但是还是不要将这二者进行混淆。

可迭代对象:

1.是一个对象,符合可迭代协议

2.可迭代协议是什么?

2.1 实现了[Symbol.iterator]作为key的方法,且这个方法返回一个迭代器对象

3.for...of...本质上就是调用了这个[Symbol.iterator]为key的方法

2.2 原生可迭代对象(JS内置)

  • String
  • Array
  • Set
  • Map
  • NodeList类数组对象
  • Arguments类数组对象

2.2.1 部分可迭代对象for...of...展示

js 复制代码
let str = 'Alice'
let arr = [1, 2, 3, 4, 5]
let map = new Map([['name', 'Alice'], ['age', 18]])

for (const element of str) {
    console.log(element);//会依次打印
}
for (const element of arr) {
    console.log(element);
}
for (const element of map) {
    console.log(element);
}

2.2.2 查看内置的[Symbol.iterator]方法

由2.2.1我们可以迭代这些对象,既然可以迭代,那必然是符合可迭代对象协议

js 复制代码
const arr = ['Alice', 'Bob', 'John']
// 数组的[Symbol.iterator]方法
let arrIterator = arr[Symbol.iterator]()
console.log(arrIterator.next())//{ value: 'Alice', done: false }
console.log(arrIterator.next())//{ value: 'Bob', done: false }
console.log(arrIterator.next())//{ value: 'John', done: false }
console.log(arrIterator.next())//{ value: undefined, done: true }

const str = 'Alice'
// 字符串的[Symbol.iterator]方法
let strIterator = str[Symbol.iterator]()
console.log(strIterator.next())//{ value: 'A', done: false }
console.log(strIterator.next())//{ value: 'l', done: false }
console.log(strIterator.next())//{ value: 'i', done: false }
console.log(strIterator.next())//{ value: 'c', done: false }
console.log(strIterator.next())//{ value: 'e', done: false }
console.log(strIterator.next())//{ value: undefined, done: true }

2.3 可迭代对象的实现

既然我们需要对对象可迭代,那么必须实现[Symbol.iterator]方法,并且这个方法返回一个迭代器对象。

js 复制代码
// 定义一个可迭代对象
let iterableObject = {
    arr: ['Alice', 'Bob', 'John'],
    [Symbol.iterator]: function () {
        let index = 0
        console.log(this)
        /**
            {
            arr: [ 'Alice', 'Bob', 'John' ],
            [Symbol(Symbol.iterator)]: [Function: [Symbol.iterator]]
            }
         */
        let _iterator = {
            next: () => {
                if (this.arr.length > index) {
                    return { done: false, value: this.arr[index++] }
                } else {
                    return { done: true, value: undefined }
                }
            }
        }
        return _iterator
    }
}

let arrIter = iterableObject[Symbol.iterator]()
console.log(arrIter.next())//{ done: false, value: 'Alice' }
console.log(arrIter.next())//{ done: false, value: 'Bob' }
console.log(arrIter.next())//{ done: false, value: 'John' }
console.log(arrIter.next())//{ done: true, value: undefined }

当我们使用for...of...对目标对象进行迭代的时候,它会自动执行[Symbol.iterator]方法进行迭代。

2.4 可迭代对象的使用场景

  • for...of...
  • 展开语法
  • 结构语法
  • promise.all(iterable)
  • promise.race(iterable)
  • Array.from(iterable)
  • ...

2.5 自定义可迭代类实现

由上面几节,我们实现了字面量可迭代对象的实现,接下来我们来设计一个可迭代类的实现。

js 复制代码
class MyInfo {
    constructor(name, age, friends) {
        this.name = name
        this.age = age
        this.friends = friends
    }
    [Symbol.iterator]() {
        let index = 0
        let _iterator = {
            next: () => {
                if (this.friends.length > index) {
                    return { done: false, value: this.friends[index++] }
                } else {
                    return { done: true, value: undefined }
                }
            }
        }
        return _iterator
    }
}

const myInfo = new MyInfo('Alice', 18, ['Amy', 'Lihua'])
for (const element of myInfo) {
    console.log(element)//Amy Lihua
}

这个MyInfo类只是简单的实现了对friends的迭代,当然你也可以迭代所有你想要迭代的对象

3.生成器

生成器是ES6新增的一种可以对函数进行控制的方案,它可以控制函数的暂停执行和继续执行。

生成器函数和普通函数的不同:

  • 普通函数是function定义,生成器是function*定义
  • 生成器函数可以通过yield来控制函数的执行
  • 生成器函数返回一个生成器(Generator),生成器是一种特殊的迭代器

3.1 生成器函数的基本实现

js 复制代码
function* test() {
    console.log('xxx');
}
// 为什么调用的函数的执行,却没有出现打印结果?
test()
console.log(test())//Object [Generator] {}

首先我们会发现,函数突然暂停了,没有项普通函数那样正常的执行打印。

我们前面也说过,他是生成器函数,执行的结果会返回一个生成器,同时也是一个特殊的迭代器。

所以和普通函数相比,他的执行好像被强行暂停了,那怎么让它继续执行呢?我们慢慢来探讨。

3.2 生成器函数的单次执行

js 复制代码
function* test() {
    console.log('xxx');
}
// 为什么调用的函数的执行,却没有出现打印结果?
test()
console.log(test())//Object [Generator] {}
console.log(test().next())//xxx  { value: undefined, done: true }

我们根据上面函数的执行结果,发现调用test()函数我们得到了一个生成器,生成器有个next方法,又能让函数继续执行从而打印了xxx,并且还有一段很熟悉的东西: { value: undefined, done: true }

这个对象我们是不是在哪见过?

没错,就是迭代器执行next函数的结果,现在就能体会为什么说生成器是个特殊的迭代器了。

这个对象表示这次迭代是最后一次,后面没值可继续迭代了

那话说回来,yield关键字去哪了?我们马上就来探讨。

3.3 生成器的多次执行

既然单次调用就是执行一次迭代器,那我们用yield看看是不是真的控制生成器的执行。

js 复制代码
function* generator() {
    console.log('开始了');
    yield 1
    console.log('进行中');
    console.log('多执行一次');
    yield 2
    console.log('结束了');
    yield 3
}

generator() //还是依然无事发生
let genera = generator()
// 执行了第一个yield,并且将yield后的值当做第一个迭代器的值,并且告知后面还能继续迭代
console.log(genera.next());//开始了  { value: 1, done: false }
// 有点意思,第二次执行了yield 1 和yield 2之间的所有语句 不是仅仅执行一行
console.log(genera.next());//进行中 多执行一次 { value: 2, done: false }
// 那不出意外 这里就是获得 结束了 以及 value为3的迭代器值
console.log(genera.next());// 结束了 { value: 3, done: false } 
// 果然 和我们预料的一致,但是这个done 还是false 后面还有?呢我们在继续执行一次
console.log(genera.next());//{ value: undefined, done: true }
// 这次再没有了

到这,我们发现了,这那叫生成器,这不就是迭代器么?

没错,这就是迭代器,只不过迭代的进行 通过函数内的yield关键字来当做开关

yield x 这个x会被当做此次迭代器的值,有没有yield会被当做done的结果 告诉我们后面 能不能继续迭代

既然叫做生成器函数,呢么这个函数的参数是如何传递的?

3.4 生成器函数的传参

我们先按照函数的传参去尝试一下

js 复制代码
function* name(name1, name2) {
    let firstName = yield name1
    let lastName = yield name2
    console.log(firstName, lastName);
    return firstName + lastName
}

console.log(name('Kaselin', 'Alice'));//Object [Generator] {}
// 还是一样的,是个生成器 函数的执行被暂停了
let getName = name('Keselin', 'Alice')
console.log(getName.next());//{ value: 'Kaselin', done: false }
// 第一次执行,获取到了name1的值
console.log(getName.next());//{ value: 'Alice', done: false }
// 第二层执行,获取到了name2的值
console.log(getName.next());//{ value: NaN, done: true }
// 喂 怎么返回了NaN?按理说不应该是字符串的拼接?
// 我们加上log函数,去尝试打印firstName和lastName,得到的都是undefined,
// 这NaN原来是undefined+undefined得来的

// 由此我们得出生成器函数的参数是分段传递的

function* name(name1, name2) {
    let firstName = yield name1
    let lastName = yield name2
    return firstName + lastName + 1
}

console.log(name('Kaselin', 'Alice'));//Object [Generator] {}
// 还是一样的,是个生成器 函数的执行被暂停了
let getName = name('Keselin', 'Alice')
console.log(getName.next());//{ value: 'Keselin', done: false }
console.log(getName.next('Amy'));//{ value: 'Alice', done: false }
console.log(getName.next('Bob'));//{ value: 'AmyBob1', done: true }
console.log(getName.next());//{ value: undefined, done: true }
console.log(getName.next());//{ value: undefined, done: true }

// 第一次next传入的Amy被舍弃,第三次未传参变为undefined
console.log(getName.next('Amy'));//{ value: 'Keselin', done: false }
console.log(getName.next('Bob'));//{ value: 'Alice', done: false}
console.log(getName.next());//{ value: 'Bobundefined1', done: true }
console.log(getName.next());//{ value: undefined, done: true }
console.log(getName.next());//{ value: undefined, done: true }

// 这里的第三次NaN实际上是因为消耗最后一个yield以及yield被消耗完的呢一次没有传参都是undefined导致的
console.log(getName.next());//{ value: 'Keselin', done: false }
console.log(getName.next());//{ value: 'Alice', done: false }
console.log(getName.next());//{ value: NaN, done: true }
console.log(getName.next('Amy'));//{ value: undefined, done: true }
console.log(getName.next('Bob'));//{ value: undefined, done: true }



// 由此我们可以得出这个传值的一般规律:
// 1.有yield接受的情况下,传入的任何参数都不会被接收
// 2.本次执行next函数消耗最后一个yield的时候,当前next传入的参数会赋值给第一个变量
// 3.yield被消耗完,还会再接受一次next传入的参数
// 4.错过这个传参窗口,后续所有参数都会舍弃

这里的生成器函数传参需要多尝试理解一下,是一个很让人费解的机制。

  1. 第一次next()调用的参数被忽略(历史原因)
  2. 后续next()调用 的参数成为上一个yield表达式的返回值
  3. 每个next()调用推进到下一个yield或return语句
  4. 生成器完成后,所有next()调用返回{value: undefined, done: true}

3.5 生成器代替迭代器

我们一直在说,生成器是一种特殊的迭代器,那生成器必定替代迭代器对象的。

js 复制代码
let friends = ['Alice', 'Bob', 'Amy']

function* createFriendsIterator(friendsArr) {
    for (const friend of friendsArr) {
        yield friend
    }
}

// let create = createFriendsIterator(friends)
// 还是个生成器
// console.log(create) // Object [Generator] {}
// console.log(create.next()) // { value: 'Alice', done: false }
// console.log(create.next()) // { value: 'Bob', done: false }
// console.log(create.next()) // { value: 'Amy', done: false }
// console.log(create.next()) // { value: undefined, done: true }

// 熟悉的结果,熟悉的配方,都是符合我们的预期的
// 能不能写的更优雅一点?还真有 yield*
// yield* 这个写法会自动为可迭代对象的结果追加yield
function* createFriendsIterator(friendsArr) {
    yield* friendsArr
}
let create = createFriendsIterator(friends)
console.log(create) // Object [Generator] {}
console.log(create.next()) // { value: 'Alice', done: false }
console.log(create.next()) // { value: 'Bob', done: false }
console.log(create.next()) // { value: 'Amy', done: false }
console.log(create.next()) // { value: undefined, done: true }
// 有没有发现结果和上面一样?

既然我们学会了这么优雅的写法来获取迭代器,那我们可以不可以对最开始的那个MyInfo类进行改造?

4.生成器改造可迭代对象

js 复制代码
class MyInfo {
    constructor(name, age, friends) {
        this.name = name
        this.age = age
        this.friends = friends
    }

    *[Symbol.iterator]() {
        yield* this.friends
    }
}

let myInfo = new MyInfo('Alice', 18, ['Bob', 'John', 'Amy'])

for (const info of myInfo) {
    console.log(info);
    // Bob
    // John
    // Amy
}


-------------------------------------------------------------
class MyInfo {
    constructor(name, age, friends) {
        this.name = name
        this.age = age
        this.friends = friends
    }
    [Symbol.iterator]() {
        let index = 0
        let _iterator = {
            next: () => {
                if (this.friends.length > index) {
                    return { done: false, value: this.friends[index++] }
                } else {
                    return { done: true, value: undefined }
                }
            }
        }
        return _iterator
    }
}

const myInfo = new MyInfo('Alice', 18, ['Amy', 'Lihua'])
for (const element of myInfo) {
    console.log(element)//Amy Lihua
}

上下一对比,简直了。

5.结论

  1. 迭代器需要满足迭代器协议,拥有自己独特的next方法,返回{done:Boolean,value:any}
  2. 可迭代对象需要符合可迭代协议,即拥有[Symbol.iterator]方法,执行这个方法后返回一个迭代器
  3. 生成器通过function*(){ }声明,yield关键字控制执行,并且返回迭代器
  4. 生成器是一种特殊的迭代器

6.结语

坚持是一件特别有趣的事,当你日复一日年复一年的积累,时间的复利就会在你的身上发生。

如有问题欢迎大家一起讨论学习,共勉。(^▽^)

相关推荐
拳打南山敬老院2 小时前
漫谈 MCP 构建之概念篇
前端·后端·aigc
前端老鹰2 小时前
HTML <output> 标签:原生表单结果展示容器,自动关联输入值
前端·html
OpenTiny社区2 小时前
OpenTiny NEXT 内核新生:生成式UI × MCP,重塑前端交互新范式!
前端·开源·agent
耶耶耶1112 小时前
web服务代理用它,还不够吗?
前端
Liamhuo2 小时前
2.1.7 network-浏览器-前端浏览器数据存储
前端·浏览器
洋葱头_2 小时前
vue3项目不支持低版本的android,如何做兼容
前端·vue.js
前端小书生2 小时前
React 组件渲染
前端·react.js
sjd_积跬步至千里3 小时前
CSS实现文字横向无限滚动效果
前端
维他AD钙3 小时前
前端基础避坑:3 个实用知识点的简单用法
前端