为啥 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() 返回一个具有donevalue 属性的新对象,而不是采用像C# IEnumerableIEnumerator 这样的协议,它不分配对象,代价是需要两次调用(一个到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 返回对象的原因是即使donetrue 也可以返回value!哇。此外,除了yieldyield*-ing 值之外,生成器还可以使用return 值,当donetrue 时,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)

除非它没有,因为即使 donefalse 迭代器可能仍然返回另一个值。考虑:

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 favoured StopIteration 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()

为啥内联 JavaScript 在引用 JavaScript 错误时起作用 [重复]

搬砖系列如何在遍历List时安全删除集合元素

为啥javascript标签使用“文本”?

为啥我的 Javascript 不起作用?

为啥 JavaScript 需要以“;”开头?