ES6:【深扒】 深入理解 JavaScript 中的生成器(1)

下面是一个 for 循环的例子,会在每次循环中输出当前的 index ,这段代码很也是简单的生成了 0-5 这些数字

for (let i = 0; i <= 5; i++) {

console.log(i);

}

// 输出 0 1 2 3 4 5

我们再来看看利用生成器函数是怎么实现的

function* generatorForLoop(num) {

for (let i = 0; i <= num; i ++) {

yield console.log(i);

}

}

const gen = generatorForLoop(5);

gen.next(); // 0

gen.next(); // 1

gen.next(); // 2

gen.next(); // 3

gen.next(); // 4

gen.next(); // 5

我们可以看到,只有调用 next 方法,才会向下执行,而不会一次产生所有值。这就是一个最简单的生成器了。在某些场景下,这种特性就成为了它的杀手锏

基本概念


1. 函数声明

生成器的形式是一个函数,函数名称前面加一个星号 * 表示它是一个生成器。

// 函数声明

function * generator () {}

// 函数表达式

let generator = function *() {}

在定义一个生成器时,星号的位置在函数名前,但是位置没有明确的要求,不需要考虑挨着谁,都可以

只要是可以定义函数的地方,就可以定义生成器。

需要特别注意的是:箭头函数不能用来定义生成器

2. yield 表达式

函数体内部使用yield表达式,定义不同的内部状态,我们来看一段代码

function* helloWorld() {

yield ‘hello’;

yield ‘world’;

return ‘ending’;

}

在上面的代码中定义了一个生成器函数 helloWorld ,内部有两个 yield 表达式,三个状态:hello,world 和 return 语句

作为生成器的核心,单纯这么解释可能还是不能明白 yield 的作用以及它的使用方法

下面我们来展开说说 yield 关键字

首先它和 return 关键字有些许的类似,return 语句会在完成函数调用后返回值,但是在 return 语句之后无法进行任何操作

image-20210812111012666

可以看到在编译器中第一个 return 语句之后的代码变灰了,说明了没有生效。但是yield的工作方式却不同,我们再来看看 yield 是如何工作的

image-20210812112332270

注意yield 关键字只能在生成器函数内部使用,其他地方使用会抛出错误

首先生成器函数会返回一个遍历器对象,只有通过调用 next 方法才会遍历下一个状态,而 yield 就是一个暂停的标志

在上面的代码中,首先声明了一个生成器函数,利用 myR 变量接收生成器函数的返回值,也就是上面所说的遍历器对象,此时遍历器对象处于暂停状态

当调用 next 方法时,开始执行,遇到 yield 表达式,就暂停后面的操作,将 yield 后面的表达式的值,作为返回的对象的 value 值,因此第一个 myR.next() 中的 value 值为 8

再次调用 next 方法时,再继续向下执行,遇到 yield 再停止,后续操作一致

需要注意的是,yield 表达式后面的表达式,只有当调用next方法,内部指针指向该语句时才会执行

function* gen() {

yield 123 + 456;

}

就例如上面的代码中,yield后面的表达式 123 + 456 ,不会立即求值,只会在 next 方法将指针移到这一句时,才会求值。

因此可以理解为 return 是结束, yield 是停止

3. 一定需要 yield 语句吗?

其实在生成器函数中也可以没有yield表达式,但是生成器的特性还在,那么它就变成了一个单纯的暂缓执行函数,只有在调用该函数的遍历器对象的 next 方法才会执行

function* hello() {

console.log(‘现在执行’);

}

// 生成遍历器对象

let generator = hello()

setTimeout(() => {

// 开始执行

generator.next()

}, 2000)

4. 注意

yield 表达式如果用在另一个表达式中,必须放在圆括号里

console.log('Hello' + (yield 123)); // OK

  • 1

yield 表达式用作函数参数可以不加括号

foo(yield 'a')

  • 1

如何理解 Generator 函数是状态机?


在阮一峰老师的 ES6 书籍上有着对生成器函数这样的理解

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

书上说,Generator 函数是状态机,这是什么意思呢,状态机又怎么理解呢?

这个和 JavaScript 的状态模式有些许关联

状态模式:当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象

看到这些定义的时候,显然每个字都知道是什么意思,合起来却不知所云

先不要慌,我们先来看看状态模式是个什么东西,写个状态机就明白了

我们用一个洗衣机的例子,按一下电源键就打开,再按一下就关闭,我们先来实现这个

let switches = (function () {

let state = “off”;

return function () {

if (state === “off”) {

console.log(“打开洗衣机”);

state = “on”;

} else if (state === “on”) {

console.log(“关闭洗衣机”);

state = “off”;

}

}

})();

在上面的代码中,通过一个立即执行函数,返回一个函数,将状态 state 保存在函数内部,每次按下电源键调用 switches 函数即可。

这样看起来很完美,下面我们改变一下需求,洗衣机上有一个调整模式的按钮,每按一下换一个模式,假设有快速、洗涤、漂洗、拖水怎么实现

同样的我们还是可以采用 if-else 语句实现

let switches = (function () {

let state = “快速”;

return function () {

if (state === “快速”) {

console.log(“洗涤模式”);

state = “洗涤”;

} else if (state === “洗涤”) {

console.log(“漂洗模式”);

state = “漂洗”;

} else if (state === “漂洗”) {

console.log(“脱水模式”);

state = “脱水”;

} else if (state === “脱水”) {

console.log(“快速模式”);

state = “快速”;

}

}

})();

越来越复杂了,当模式再增多时,if-else 语句会越来越多,代码会难以阅读,你可能会说可以采用 switch-case 语句来实现,当然也可以,但是治标不治本。我们可不可以不采用判断语句实现呢。回到我们刚开始的定义

状态模式:当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象

咦,想想,洗衣机不正是需要实现状态改变,行为改变吗?那这正可以采用状态模式来实现呀,这里我们就直接引出我们的 generator 函数,通过控制状态来改变它的行为

利用原型来实现的方法太过于复杂和冗余了,就不展示了

const fast = function () {

console.log(“快速模式”);

}

const wash = function () {

console.log(“洗涤模式”);

}

const rinse = function () {

console.log(“漂洗模式”);

}

const dehydration = function () {

console.log(“脱水模式”);

}

function* models() {

let i = 0,

fn, len = arguments.length;

while (true) {

fn = arguments[i++]

yield fn()

if (i === len) {

i = 0;

}

}

}

const exe = models(fast, wash, rinse, dehydration); //按照模式顺序排放

在上面的代码中我们只需要在每次按下时调用 next 方法即可切换下一个状态

生成器

说了这么多 generator 为什么说是状态机呢?我的理解是:当调用 Generator 函数获取一个迭代器时,状态机处于初态。迭代器调用 next 方法后,向下一个状态跳转,然后执行该状态的代码。当遇到 return 或最后一个 yield 时,进入终态。同时采用 Generator 实现的状态机是最佳的结构。

next 传递参数


生成器的另一强大之处在于内建消息输入输出能力,而这一能力仰仗于 yieldnext 方法

yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数。

来看一个例子

function* foo(x) {

let y = x * (yield)

return y

}

const it = foo(6)

it.next()

let res = it.next(7)

console.log(res.value) // 42

在上面的代码中,调用 foo 函数返回一个遍历器对象 it ,并将 6 作为参数传递给 x ,调用遍历器对象的 next 方法,启动遍历器对象,并且运行到第一个 yield 位置停止,

再次调用 next 方法传入参数 7 ,作为上一个 yield 表达式的返回值也就是 x 的乘项 (yield) 的值,运行到下一个 yieldreturn 结束

下面开始作死

在上面的例子中,如果不传递参数会这么样呢?

在第二次运行 next 方法的时候不带参数,导致了 y 的值等于 6 * undefined 也就是 NaN 所以返回的对象的 value 属性也是 NaN

image-20210812182445067

我们再变一下

在原先的例子中,我们说第一个 next 是用来启动遍历器对象,那么如果传入参数会怎么样?

其实这样传递参数是无效的,因为我们说 next 方法的参数表示上一个 yield 表达式的返回值。

V8 引擎直接忽略第一次使用 next 方法时的参数

与 Iterator 接口的关系


在上一篇中我们知道,一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回一个遍历器对象

在这一篇我们知道,生成器函数就是遍历器生成函数,那么是不是有什么想法了呢?

我们可以把生成器赋值给对象的 Symbol.iterator 属性,实现 iterator 接口

let myIterable = {};

myIterable[Symbol.iterator] = function* () {

yield 1;

yield 2;

yield 3;

}

[…myIterable] // [1, 2, 3]

提前终止生成器


生成器函数返回的遍历器对象,都有 next 方法,以及可选的 return 方法和 throw 方法

我们先来看 return 方法

return

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+点击【学习资料】即可免费领取!

了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**

[外链图片转存中…(img-qnlJTXlr-1713485388210)]

[外链图片转存中…(img-niKa2cAe-1713485388211)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-KRYfaj3K-1713485388211)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

[外链图片转存中…(img-tPTHuhgk-1713485388212)]

最后

由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+点击【学习资料】即可免费领取!

[外链图片转存中…(img-blrhD418-1713485388212)]

[外链图片转存中…(img-Y7r2WE8c-1713485388212)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值