为啥 Javascript `iterator.next()` 返回一个对象?
Posted
技术标签:
【中文标题】为啥 Javascript `iterator.next()` 返回一个对象?【英文标题】:Why does Javascript `iterator.next()` return an object?为什么 Javascript `iterator.next()` 返回一个对象? 【发布时间】:2019-05-22 12:11:07 【问题描述】:救命!在用 C# 编程很长一段时间后,我正在学习爱上 javascript,但我一直在学习爱上可迭代协议!
为什么 Javascript 采用protocol 要求为每次迭代创建一个新对象?为什么next()
返回一个具有done
和value
属性的新对象,而不是采用像C# IEnumerable
和IEnumerator
这样的协议,它不分配对象,代价是需要两次调用(一个到moveNext
到看看迭代是否完成,然后再到current
获取值)?
是否存在跳过next()
分配对象返回的底层优化?很难想象由于可迭代对象不知道返回后如何使用该对象...
生成器似乎不重用下一个对象,如下图所示:
function* generator()
yield 0;
yield 1;
var iterator = generator();
var result0 = iterator.next();
var result1 = iterator.next();
console.log(result0.value) // 0
console.log(result1.value) // 1
嗯,here's 一条线索(感谢 Bergi!):
稍后我们将回答一个重要问题(在 3.2 节中):为什么迭代器(可选)可以在最后一个元素之后返回一个值?这种能力是元素被包装的原因。否则,迭代器可以简单地在最后一个元素之后返回一个公开定义的标记(停止值)。
在教派中。 3.2 他们讨论使用Using generators as lightweight threads。似乎是说从next
返回对象的原因是即使done
是true
也可以返回value
!哇。此外,除了yield
和yield*
-ing 值之外,生成器还可以使用return
值,当done
是true
时,return
生成的值最终与value
一样!
所有这些都允许伪线程。而这个特性,伪线程,值得为每次循环分配一个新对象...... Javascript。总是那么出人意料!
尽管现在我想了想,允许yield*
“返回”一个值以启用伪线程仍然不能证明返回一个对象是合理的。 IEnumerator
协议可以扩展为在moveNext()
返回false
之后返回一个对象——只需添加一个属性hasCurrent
在迭代完成后测试true
表示current
有一个有效值。 ..
而且编译器的优化也很重要。这将导致迭代器的性能出现相当大的差异......这不会给库实现者带来问题吗?
所有这些观点都在友好的 SO 社区发现的this thread 中提出。然而,这些论点似乎站不住脚。
然而,不管是否返回一个对象,没有人会在迭代“完成”后检查一个值,对吧?例如。大多数人都会认为以下内容会记录迭代器返回的所有值:
function logIteratorValues(iterator)
var next;
while(next = iterator.next(), !next.done)
console.log(next.value)
除非它没有,因为即使 done
是 false
迭代器可能仍然返回另一个值。考虑:
function* generator()
yield 0;
return 1;
var iterator = generator();
var result0 = iterator.next();
var result1 = iterator.next();
console.log(`$result0.value, $result0.done`) // 0, false
console.log(`$result1.value, $result1.done`) // 1, true
在“完成”之后返回值的迭代器真的是迭代器吗?一只手拍的声音是什么?只是看起来很奇怪......
here 是关于我喜欢的生成器的深入帖子。与迭代集合的成员相比,很多时间都花在控制应用程序的流程上。
另一种可能的解释是 IEnumerable/IEnumerator 需要两个接口和三个方法,而 JS 社区更喜欢单一方法的简单性。这样他们就不必引入符号方法组的概念,也就是接口......
【问题讨论】:
您能否链接到说明需要返回 new 对象的规范? 您可能不会在这里得到有关特定语言设计决策的答案,因为从事规范工作的人不在这里。您应该直接与他们联系。 @Bergi:实际上,这仅描述了内置迭代器的行为。协议本身似乎不需要在每次迭代中都有一个新对象。 FWIW,这是一个重用结果对象的示例:jsfiddle.net/wp82n07o。 specification of the protocol 似乎不需要在每次迭代中返回一个 不同的 对象(据我所知)。所以看起来你可以只分配一个。但是,正如我之前提到的,如果您想对此进行澄清,我会联系 TC39 委员会的人员。 @FelixKling 这里有一些讨论:esdiscuss.org/topic/…,esdiscuss.org/topic/iterator-next-method-returning-new-object。我还发现重用对象会使编译器更难进行转义分析... 【参考方案1】:是否存在跳过
next()
分配的对象返回的底层优化?
是的。那些迭代器结果对象很小并且通常是短暂的。特别是在for … of
循环中,编译器可以进行简单的转义分析,以查看对象根本不面向用户代码(而只是内部循环评估代码)。它们可以由垃圾收集器非常有效地处理,甚至可以直接在堆栈上分配。
这里有一些来源:
JS inherits it functionally-minded iteration protocol from Python,但 with results objects 而不是 the previously favouredStopIteration
exceptions
Performance concerns in the spec discussion (cont'd) 耸了耸肩。如果您实现自定义迭代器并且速度太慢,请尝试使用生成器函数
(至少对于内置迭代器)these optimisations are already implemented:
实现出色迭代性能的关键是确保循环中对
iterator.next()
的重复调用得到良好优化,并在理想情况下使用存储加载传播等高级编译器技术完全避免分配iterResult
,逃逸分析和聚合的标量替换。为了真正发挥性能,优化编译器还应该完全消除iterator
本身的分配 -iterable[Symbol.iterator]()
调用 - 并直接对可迭代的后备存储进行操作。
【讨论】:
酷!我可以在调用站点看到编译器可以确定对象是短暂的,但是被调用者/生成者如何知道不分配对象?还是编译器会生成两个版本的被调用者/生成器,一个返回一个对象,另一个使用无分配协议? @ChristopherKing Tbh,我不知道。我在上面写的只是推测,这是我们可以合理地从编译器中得到的期望,以解释为什么指定的协议没有你说的那么糟糕。我将搜索有关引擎是否真正实现这些优化的权威来源... 谢谢。运行时可以看到它的寿命很短,因此将其放入 gen0 堆中,但仍然 - 为循环的每次迭代在堆上分配一个对象... @ChristopherKing JS 每秒运行数百万个对象或其他东西。所以我猜你关心的是微/纳米优化。 @ChristopherKing 他们选择让迭代协议简单放在首位,只指定它应该如何工作,并信任引擎实现来优化。 (规范中的语义越简单,编写的代码越简单,编译器就越容易优化)。请注意,ECMAScript 规范根本没有讨论分配/解除分配。【参考方案2】:Bergi 已经回答了,我已经投了赞成票,我只想补充一下:
您为什么还要担心返回的新对象?它看起来像:
done: boolean, value: any
你知道,无论如何你都将使用value
,所以这真的不是额外的内存开销。还剩下什么? done: boolean
和对象本身每个占用最多 8 个字节,这是可能的最小可寻址内存,必须由 cpu 处理并在几皮秒或纳秒内分配到内存中(我认为它是 pico- 考虑到可能存在的v8 优化)。现在,如果您仍然关心浪费 那 量的时间和内存,那么您真的应该考虑从 JS 切换到 Rust+WebAssembly 之类的东西。
【讨论】:
开销是对象本身......但也许你是对的。如果一个人正在编写 Javascript,也许根本不应该担心内存压力。尽管如此,即使对于动态语言来说,为每次迭代分配一个对象似乎也是内存暴涨!以上是关于为啥 Javascript `iterator.next()` 返回一个对象?的主要内容,如果未能解决你的问题,请参考以下文章
STL 2—迭代器相关运算——advance(),distance(),next(),prev()