JavaScript 核心原理解析
周爱民
《JavaScript 语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
32699 人已学习
新⼈⾸单¥59
登录后,你可以任选3讲全文学习
课程目录
已完结/共 28 讲
开篇词 (1讲)
JavaScript 核心原理解析
15
15
1.0x
00:00/00:00
登录|注册

10 | x = yield x:迭代过程的“函数式化”

思考题
传入参数的改造
.next()方法
执行现场
生成器函数
迭代过程展开
生成器函数与yield运算

该思维导图由 AI 生成,仅供参考

你好,我是周爱民。欢迎回到我的专栏。
相信上一讲的迭代过程已经在许多人心中留下了巨大的阴影,所以很多人一看今天的标题,第一个反应是:“又来!”
其实我经常习惯用同一个例子,或者同类型示例的细微不同去分辨与反映语言特性上的核心与本质的不同。如同在第 2 讲第 3 讲中都在讲的连续赋值,看起来形似,却根本上不同。
同样,我想你可能也已经注意到了,在第 5 讲(for (let x of [1,2,3]) ...)和第 9 讲((...x))中所讲述的内容是有一些相关性的。它们都是在讲循环。但第 5 讲主要讨论的是语句对循环的抽象和如何在循环中处理块。而第 9 讲则侧重于如何通过函数执行把(类似第 5 讲的)语句执行重新来实现一遍。事实上,仅仅是一个“循环过程”,在 JavaScript 中就实现了好几次。这些我将来都会具体地来为你分析。
至于今天,我还是回到函数的三个语义组件,也就是“参数、执行体和结果”来讨论。上一讲本质上讨论的是对“执行体”这个组件的重造,今天,则讨论对“参数和结果”的重构。

将迭代过程展开

通过上一讲,你应该知道迭代器是可以表达为一组函数的连续执行的。那么,如果我们要把这一组函数展开来看的话,其实它们之间的相似性是极强的。例如上一讲中提到的迭代函数foo(),当你把它作为对象 x 的迭代器符号名属性,并通过对象 x 来调用它的迭代展开,事实上也就相当于只调用了多次的 return 语句。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

生成器函数和yield运算符为实现迭代过程的“函数式化”提供了新的思路和方法。文章首先解释了迭代器可以被表达为一组函数的连续执行,然后介绍了生成器函数通过yield运算符实现了多次进入和退出的功能,从而更灵活地处理参数和结果。生成器函数封装了循环过程,使得循环的实现更加灵活和简洁。文章还详细讨论了生成器函数内部的执行现场和执行上下文的管理,以及生成器的特殊之处:生成器的上下文(多数时间是)在栈的外面。文章还解释了生成器函数执行到yield表达式时的行为,以及生成器对象在创建时的状态和执行上下文信息。通过讲解生成器函数和yield运算符的使用,为读者提供了一种新的思路和方法,来更好地理解和处理迭代过程中的函数式编程。文章还介绍了生成器对“函数执行”的执行体加以改造,使之变成由tor.next()管理的多个片断,以及对传入参数的改造,使执行“每个body”时可以接受不同的参数。文章还提到了“委托的yield”这个话题,鼓励读者进行深入探索。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JavaScript 核心原理解析》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(10)

  • 最新
  • 精选
  • 授人以摸鱼
    我忽然想明白为啥会有迭代器的next需要支持传入参数这样的功能了……以前一直没想明白来着…… 其实就是作为生成器函数的一个应用实例,co模块需要这个功能,需要把yield返回的promise like对象的then方法传回的值从next给生成器函数传回去,这个需求抽象一下,就成了“外部执行环境会需要根据yield传出的结果进行变换后用next传入”这样的通用需求了。 所以我同时也就理解了,为啥说async await是生成器函数的语法糖了,而且这糖真甜wwwww

    作者回复: :) 赞的!又是大有所得~ ^^.

    2020-03-26
    11
  • 红白十万一只
    老师,最近遇到个问题 if (true) { a = 5 function a() { } a = 1 console.log(a) } console.log(a) 1 5 外部的a变成了5,内部的a变成了1 我查了资料说: function a() { }的函数提升只提升到了if的代码块顶部,但也有一个var声明被悬挂到了全局中值为undefined 之后在a=5时if内部的变量a从函数被修改为了5 function a() { }这步计算函数声明时,函数对象被分配给函数作用域变量。因为被修改为5,所以此时外部的a也被改变成了5 之后在a=1时if内部的变量a从5被修改为了1 最后输出1 5 我的理解是 函数提升只提升到代码块顶部,但是为了符合函数作用域的规则在最近的函数作用域创建了一个同名变量且值为undefined。 之后再执行到声明语句时将这个函数对象转换为值赋值给这个函数作用域同名变量。 老师有更详细的解释么?我找了ES规范没有发现对这里解释。

    作者回复: 这个东西说起来,就有点历史了。呵呵 早期的JScript认为,只要在同一个作用域(函数/全局)内声明函数,那么同名函数是覆盖的,也就是后一个声明覆盖前一个。所以按照这个逻辑,对“同一个名字的”连续两次声明其实只有后一个有效,并且即使是在前一个之前访问它,也是一样的(因为有变量提升,所以这个域中能访问到的只有第二个声明)。 这个特性直到较晚一些的IE中都是如此,包括IE8或者IE9,但是IE10之后我就不知道了。然而Firefox/SpiderMonkey却不这么认为,在早期还没有块级作用域的时候,他们就搞了一个称为“条件声明”的东西出来,也就是在if的两个分支中如果声明了函数,只有执行到指定分支时,该分支中的声明才有效。——因为那时还没有“块级作用域”这么个概念,所以这个就算成了动态的、“执行期的”逻辑。 而你的这个例子,就是这个特性在起作用了。——在执行到function a()这个声明之前,会因为变量泄露而在全局创建标识符`a`;而在执行到function a()的时候,就在当前块中创建了函数a()。 这个特性呢,在标准的ECMAScript(的正文部分)中是没有的。因为对于Firefox来说,这是一个特殊的、自有的特性,不属于ECMAScript规范,但由于它一直存在于SpiderMonkey系的引擎中,所以在MDN中也一直有这一部分的解释。 后来在ECMAScript中,因为这是一个“通常应于浏览器环境”的特性,所以把它归在了“Web Legacy Compatibility”中,是作为扩展特性来介绍的,不在ECMAScript的正文中。这与两个主题有关: > https://tc39.es/ecma262/#sec-block-level-function-declarations-web-legacy-compatibility-semantics > https://tc39.es/ecma262/#sec-functiondeclarations-in-ifstatement-statement-clauses 你仔细读读就好了。至于MDN,现在也有关于这个内容的条目,并且也指出在不同的浏览器环境中并不一致(因为是ECMAScript扩展规范,并不强制引擎实现)。在这里: > https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function#Conditionally_created_functions

    2020-03-08
    3
    5
  • 许童童
    如果遇到 yield* 就将当前的yield执行权交到 yield* 里面,yield* 里面return的值,将返回给外层的x = yield* xxx 中的x

    作者回复: Yes. 赞的!+2

    2019-12-04
    5
  • .Alter
    老师好,我想问一下生成器这个挂起和调用栈移动的机制是协程吗?

    作者回复: 不是。 不过实现上,可以是。OS真实线程的切换成本高,在实现上用协程来做是可以的。但这与具体引擎的选择有关。另外按照ECMAScript的约定,这里自己实现一个上下文的管理器也是可以的,与线程什么的,并没有关系。

    2019-12-09
    2
    4
  • 油菜
    老师,从返回结果比较,“迭代器函数”和“生成器函数”的作用是一样的,都是通过迭代器或生成器的.next()方法,一次调用获取一个值。

    作者回复: 严格来说,并没有所谓的“迭代器函数”。所谓“迭代”是一组接口规范,它约定的是一种行为。——在《JavaScript语言精髓与编程实践》中称“(可迭代对象是)一种可循环处理的‘可计算对象’”,并称“处理这种对象的‘行为’为迭代”。 而“生成器(函数)”是一个实现。

    2020-11-13
    2
  • HoSalt
    「如果用户代码——通过任意的手段——试图挂起这惟一的执行上下文,那么也就意味着整个的 JavaScript 都停止了执行」 老师,这是什么意思?唯一的执行上下文指全局上下文?

    作者回复: 不完全正确。 js的执行上下文是放在一个执行栈里面,而所谓的全局上下文只不过是在这个栈的栈底。上下文之间是通过类似arguments.caller的方式关联起来的(对的,在严格模式中没有,但内部机制上还是有的)。 所以“挂起这惟一的执行上下文”更确切的说法是“活动的(active)上下文”。活动上下文是通过栈顶的移入移出操作来切换的,如果用户代码“通过别的方法”使得这个上下文挂起了,那么整个上下文的栈就挂起了,没有办法来激活它。 js里面所谓的“单线程”,本质上就是“所有的上下文中有且仅有一个活动的(active)上下文”。如果引擎中存在多个这样的活动上下文,那么要么是多线程的,要么是多引擎的,要么是多虚拟机(vm)的。总之这些活动上下文会被隔离在不同的“全局”里面,做不到真并行。 在js的单一线程中,所谓的“(一般意义上的)全局上下文”是惟一的,但它不一定是“活动的(active)”,当然活动的上下文可能在一个函数中(当前函数),或者一个被launch起来的promise的reactions(就是.then里的回调)中,等等。但无论是哪种情况,它们“所处于的调用栈”的栈底,总会是一个全局环境(称为GlobalEnv,是用全局对象来作为词法环境的),但不一定是被称为“全局上下文”的那一个。——也就是说,所谓“一般意义上的全局上下文”,并不一定处于活动上下文(栈)的底部。

    2020-05-19
    2
  • 阿鑫
    我的理解就是 tor 这个句柄其实就是包含了这个迭代器的一切,包括上下文 context 和执行函数。每次执行 tor.next() 就是把 context 压入栈顶,然后执行执行函数?

    作者回复: 确实,ECMAScript就是这样做的。 如果tor是一个生成器对象,那么它就会有[[GeneratorContext]]这个私有槽,而tor.next()方法就是从这个私有槽中取出上下文给塞回到栈上。

    2019-12-04
    2
  • 行问
    x = yield x 首先,yield 是向函数外发送 x 的值 其次,yield 接收外部传入的参数并赋值给 x 解惑了之前理解 yield 是一个“等待”的过程,没有往“挂起”去构思 时不时会用到 async await 来写并行的 Promise, 但 yield 只知其知识点和应用,还没有开发中实际使用过

    作者回复: Promise比await要难用一点,但其实深刻理解promise对整个的语言学习提升很大很大,因为它提供了一种新的理解程序执行逻辑的模式。不过这些内容,会放到20讲之后才讨论,这一次的课程中是不包括的。:)

    2019-12-04
    2
    1
  • 静坐常思己过,闲谈莫论人非
    如果我能搞懂这些就不是初级前端了,学路漫漫感谢爱民老师

    作者回复: ������ 谢谢������

    2020-09-28
  • 潇潇雨歇
    先看看yield再来
    2019-12-04
收起评论
显示
设置
留言
10
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部