JavaScript深入之从生成器到迭代器

引言

生成器函数和迭代器是ES6 中引入的特性,它们可以用于遍历数据集合,使代码更加简洁和易于阅读。如果你目前对这两者概念并不清晰,不清楚有何用处及应用场景,那么相信此篇文章你值得一看,希望对你有所帮助。

迭代器

迭代器(iterator),是用户在容器对象(例如链表或数组)上遍历访问的对象,使用该接口无需关心对象的内部实现细节。简单来说,迭代器是帮助我们对某个数据结构进行遍历的对象 。哎这么说来它不就是是遍历方法嘛,类似于for forEach这些,可以这么理解,不过这个对象需要符合迭代器协议,迭代器协议定义了产生一系列值的标准方式,在JS中这个标准就是一个特定的next方法。

这个next方法为一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:

  • done(boolean)如果迭代器可以产生序列中的下一个值,则为false。如果迭代器已将序列迭代完毕,则为true。
  • value 迭代器返回的任何JavaScript值,done为true时可省略。

有了这个已知的定义标准我们自己尝试写一下,现在我们要迭代一个对象:

javascript 复制代码
const data = ['data1', 'data2', 'data3']

let index = 0

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

好消息这样确实可以调用成功,坏消息这只是针对于data的迭代器,如果换一个对象可能就行不通了。然后我们又发现简单的数组的迭代遍历都是这样,哎那这不就可以尝试封装成一个方法嘛:

javascript 复制代码
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 }
            }
        }
    }
}

哎这下总算是放到一起了吧,但是问题又来了数组和迭代器是分开的,相当于两个对象,我们并没有给放到一起。

那我们有没有办法将他们放到一起呢?

kotlin 复制代码
const info = {
	data: ['data1', 'data2', 'data3']
}

此时data不能迭代,我们给info创建一个迭代器,迭代info中的data

lua 复制代码
let index = 0

const infoIterator = {
    next: function() {
        if (index < info.data.length) {
            return { done: false, value: info.data[index++] }
        } else {
            return { done: true, value: undefined }
        }
    }
}
console.log(infoIterator.next())
console.log(infoIterator.next())
console.log(infoIterator.next())

此时看代码和前面是没什么区别的,依旧是分开的,我们试着把下面的代码移入到上面,但是要怎么移呢?

javascript 复制代码
let index = 0
const info = {
    data: ['data1', 'data2', 'data3'],

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

const iterator = info[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

哎这个[Symbol.iterator]是什么东西?这就和我们接下来要讲到的可迭代对象有关了

可迭代对象

当我们这样编写完,这时候我们可以把info称为可迭代对象。成为可迭代对象条件:

  1. 必须实现一个特定的函数:[Symbol.iterator]
  2. 这个函数需要返回一个迭代器(这个迭代器用于迭代当前的对象)

所以我们也可看出可迭代对象和迭代器其实不是一个东西。

我们在开发过程中迭代某一个对象的时候可能并不是想迭代对象中的data/xxx这种属性,而是迭代对象内存放的键值对。

javascript 复制代码
let index = 0
const info = {
    name: '1122',
    age: 18,

    [Symbol.iterator]() {
        const keys = Object.keys(this) // key
        const values = Object.values(this) // value
        const entries = Object.entries(this) // key - value
        let index = 0

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

const iterator = info[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

应用

可迭代对象应用:

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

另外需要注意,当对象不可迭代而使用了相关语法时会报错:Uncaught TypeError: Found non-callable @@iterator

自定义类的迭代

kotlin 复制代码
class Person {
    constructor(name, age, height, hobby) {
        this.name = name
        this.age = age
        this.height = height
        this.hobby = hobby
    }
    // 添加实例方法
    [Symbol.iterator]() {
        let index = 0

        const iterator = {
            next: () => {
                if (index < this.hobby.length) {
                    return { done: false, value: this.hobby[index++] }
                } else {
                    return { done: true, value: undefined }
                }
            }
        }
        return iterator
    }
}

const p1 = new Person('111', 18, 175, ["hobby1", "hobby2", "hobby3"])
const p2 = new Person('222', 20, 178, ["hobby1", "hobby2", "hobby3"])

迭代器的中断

在某些情况下迭代器会在没有完全迭代的情况下中断,例如遍历过程通过breakreturnthrow中断循环,或着在解构的时候没有解构所有的值等,下面用break进行举例:

kotlin 复制代码
class Person {
    constructor(name, age, height, hobby) {
        this.name = name
        this.age = age
        this.height = height
        this.hobby = hobby
    }
    // 添加实例方法
    [Symbol.iterator]() {
        let index = 0

        const iterator = {
            next: () => {
                if (index < this.hobby.length) {
                    return { done: false, value: this.hobby[index++] }
                } else {
                    return { done: true, value: undefined }
                }
            },
            return: () => {
                console.log(`迭代器中断`)
                return { done: true } // 如果没有返回对象控制台会报错Uncaught TypeError: Iterator result undefined is not an object

            }
        }
        return iterator
    }
}


const p1 = new Person('111', 18, 175, ["hobby1", "hobby2", "hobby3"])
const p2 = new Person('222', 20, 178, ["hobby1", "hobby2", "hobby3"])
for(const item of p1) {
    console.log(item)
    if(item === "hobby2") {
        break
    }
}

此时控制台输出:hobby1 hobby2 迭代器中断

生成器

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等(平时我们会编写很多函数,这些函数终止的条件通常是返回值或者发生了异常)。

在详细了解生成器前我们先了解下生成器函数。

生成器函数看名字也知道它是一个函数,那它和普通函数有什么区别呢?

  1. 生成器函数需要在function后面加一个符号 *
  2. 生成器函数可以通过yield关键字来控制函数的执行过程
  3. 生成器函数的返回值是一个generator(生成器)对象

我们用代码来体现下:

javascript 复制代码
function* foo() {
    console.log(`语句1`)
    yield console.log(`这个会执行吗?`)
    console.log(`语句2`)
    console.log(`语句3`)
    yield
    console.log(`语句4`)
    console.log(`语句5`)
}

const generator = foo()
generator.next()

此时控制台输出:语句1 这个会执行吗?

我们再次调用next,输出语句2 语句3,再调用输出语句4 语句5

我们可以看到要想执行函数内部的代码,需要生成器对象调用它的next操作,并且代码遇到yield时,就会中断执行,但是yield后面紧跟着的代码会执行。生成器有next?那它属于迭代器吗?是的,生成器事实上是一种特殊的迭代器

生成器返回值及参数

javascript 复制代码
function* foo() {
    console.log(`语句1`)
    yield "aaa"
    console.log(`语句2`)
    console.log(`语句3`)
    yield "bbb"
    console.log(`语句4`)
    console.log(`语句5`)
    yield "ccc"
}

const generator = foo()

上面代码调用next()结果输出分别是

{ value: "aaa", done: false }

{ value: "bbb", done: false }

{ value: "ccc", done: true }

但是一旦中途return一个语句,那么done为立即为true,value值为return的数值。且之后哪怕有中断语句,其返回值都是undefined。

参数

javascript 复制代码
function* foo(arg1) {
    console.log(arg1) // diaoyong1
    console.log(`语句1`)
    const arg2 = yield console.log(`这个会执行吗?`)
    console.log(`语句2`, arg2) // diaoyong2
    console.log(`语句3`)
    const arg3 = yield
    console.log(`语句4`, arg3)
    console.log(`语句5`)
}

const generator = foo("diaoyong1")
generator.next()
generator.next("diaoyong2")
generator.next("diaoyong3")

生成器函数提前结束

  1. 可以直接在代码里return
  2. 直接调用return方法:generator.return()
  3. 抛出异常

生成器代替迭代器

lua 复制代码
const data = ['data1', 'data2', 'data3']


function* createArrayterator(arr) {
    for (let i = 0; i < arr.length; i++) {
        yield arr[i]
    }
}

const dataIterator = createArrayterator(data)
console.log(dataIterator.next()) // { value: "data1", done: false }
console.log(dataIterator.next()) // { value: "data2", done: false }
console.log(dataIterator.next()) // { value: "data3", done: false }
console.log(dataIterator.next()) // { value: undefined, done: true }

不过这种方式还是比较复杂的 有没有再简洁一点的方式呢?

生成器yidld语法糖

lua 复制代码
function* createArrayterator(arr) {
    yield* arr
}

const rangeIterator = createArrayterator(data)
console.log(rangeIterator.next())
console.log(rangeIterator.next())
console.log(rangeIterator.next())

我们可以使用yield来生产一个可迭代对象,也就是yield后面跟一个可迭代对象,这时候相当于是一种yield语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中一个值

javascript 复制代码
class Person {
    constructor(name, age, height, hobby) {
        this.name = name
        this.age = age
        this.height = height
        this.hobby = hobby
    }
    // 添加实例方法
    *[Symbol.iterator]() {
        yield* this.hobby
    }
}

const p1 = new Person('111', 18, 175, ["hobby1", "hobby2", "hobby3"])
const p2 = new Person('222', 20, 178, ["hobby1", "hobby2", "hobby3"])
for(const item of p1) {
    console.log(item)
}

// 或者

const pIterator = p[Symbol.iterator]()
console.log(pIterator.next())
console.log(pIterator.next())
console.log(pIterator.next())
console.log(pIterator.next())
相关推荐
李鸿耀6 分钟前
仅用几行 CSS,实现优雅的渐变边框效果
前端
码事漫谈26 分钟前
解决 Anki 启动器下载错误的完整指南
前端
im_AMBER1 小时前
Web 开发 27
前端·javascript·笔记·后端·学习·web
蓝胖子的多啦A梦1 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码1 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo1 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.2 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
一壶浊酒..2 小时前
ajax局部更新
前端·ajax·okhttp
苏打水com2 小时前
JavaScript 面试题标准答案模板(对应前文核心考点)
javascript·面试
Wx-bishekaifayuan3 小时前
基于微信小程序的社区图书共享平台设计与实现 计算机毕业设计源码44991
javascript·vue.js·windows·mysql·pycharm·tomcat·php