Javascript 迭代器的技术定义是什么,如何测试迭代器?

5

我一直在实现一个有用的ES6 Set子类。对于我的许多新方法,我想接受一个参数,该参数可以是另一个Set或Array,或者任何我可以迭代的东西。在接口中,我一直称之为“可迭代对象”,并仅使用.forEach()对其进行操作(这对于Set或Array都有效)。示例代码:

// remove items in this set that are in the otherIterable
// returns a count of number of items removed
remove(otherIterable) {
    let cnt = 0;
    otherIterable.forEach(item => {
        if (this.delete(item)) {
            ++cnt;
        }
    });
    return cnt;
}

或者

// add all items from some other iterable to this set
addTo(iterable) {
    iterable.forEach(item => {
        this.add(item);
    });
}

然而,我怀疑我的代码并未按照ES6定义的 iterable 的方式来支持任何可迭代对象,所以我很想了解JavaScript iterable 的真正定义,就像ES6规范中使用这个术语的方式是什么?

在ES6 JavaScript中如何测试它?

如何迭代一个通用的可迭代对象?

我在ES6规范中发现了这样的短语:

如果参数iterable存在,则应该是一个实现@@iterator方法的对象, 该方法返回一个迭代器对象,该迭代器对象生成一个类似于数组的两个元素组成的对象, 其中第一个元素是将用作WeakMap键的值,第二个元素是与该键关联的值。

但那是指 @@iterator 方法,我似乎无法通过该属性名称访问它。


那个“@@iterator”不是应该是一个“system”符号实例之类的东西吗?编辑我想它应该是Symbol.iterator - Pointy
@Pointy - 也许吧,但我不太确定这些符号是如何工作的,以及我应该如何使用它或者这对我如何迭代对象有什么影响。 - jfriend00
1
Well Symbol实例作为对象属性名称运行。它们从不可枚举。所以你可以这样做 foo[Symbol.iterator] = function() ... (或者将其放在原型上)。 - Pointy
@Bergi - 我认为其他问题没有解释如何迭代发现是可迭代的东西。它确实有助于解释Symbol.iterator的问题。 - jfriend00
我认为我们只需要在问题中提出实际的问题,如果回答者想要使用你找到的引用来解释@@iterator的含义,可以留给他们自己讨论 - 或者不讨论它,但是提供一个链接。 - Bergi
显示剩余4条评论
2个回答

9

在ES6规范中,Javascript可迭代的真正定义是什么?

§25.1.1.1 定义了 "可迭代接口"。

它们是带有 Symbol.iterator 键方法的对象,该方法返回一个有效的 迭代器(反过来又是一个按照 §25.1.1.2 应该表现出来的对象)。

如何在ES6 Javascript中测试它?

我们无法在不调用 @@iterator 方法的情况下测试其返回值,也无法测试结果是否符合 Iterator 接口而不尝试运行它。最好的方法是执行:

function looksIterable(o) {
    return typeof o[Symbol.iterator] == "function";
}

然而,我通常不会测试它是否可迭代,而是让它在不可迭代时引发异常。

如何迭代通用可迭代对象?

不要使用 forEach(事实上,在 ES6 中任何地方都不要使用 forEach)。

正确的迭代方式是使用 for (… of …) 循环。它会检查所有可迭代性(使用抽象 GetIterator 操作 运行(甚至关闭)迭代器,并在非可迭代值上使用时抛出适当的 TypeError)。


1
我去转换一些.forEach()代码为for/of。大部分都很容易转换,但是使用.forEach()提供的index参数的函数需要额外的代码来跟踪索引。我不确定我是否已经准备好说应该永远不使用.forEach()。当您不使用.forEach()index参数时,在ES6中显然没有理由使用它。我最初使用.forEach()的原因之一是它可以在ES5中与polyfill集合一起使用,而for/of无法进行polyfill(必须进行转译)。 - jfriend00
是的,当然可以用那种方式实现(或者其他多种方式)。但是当您需要每次迭代时同时获取值和索引时,它们并不比 .forEach() 更简单。为什么您如此强烈反对使用 .forEach() 呢?毕竟它是最简单的匹配您所需功能的方法。难道您只是想避免它使用的多个函数调用吗? - jfriend00
1
是的,回调调用的开销是其中一个原因(诚然,我不知道它与生成器的开销相比如何,并且也不知道哪个更可能得到优化器的关注)。另外两个原因是:可迭代对象比“forEachables”更通用(更好),而且我绝对讨厌在forEach中进行副作用编程(特别是在选择mapreduce更合适的情况下),因此我更喜欢使用循环来实现命令式风格,这是与纯函数式风格有用的区别。也许我应该写一篇“forEach被认为是有害的”文章 :-D - Bergi
2
从字里行间看,我感觉你讨厌人们出于错误的原因使用 .forEach()(例如,而不是使用 .map().reduce()),现在我们有了 for/of(如果您在 ES6 环境或转译中编程),甚至更少的理由使用 .forEach(),但实际上听起来你没有一个合法的理由完全摆脱它,当它是与你正在做的事情最简单的匹配时。 - jfriend00
1
是的,我想这很好地概括了,我只是养成了说“*根本不要使用forEach*”的习惯,这对于99%的ES6用例来说是有效的,而不是详细说明它在哪些情况下是合法的(可能只有像readLines().map(…).filter(…).forEach(writeLine)或者当我的按键特别珍贵时)。 - Bergi
显示剩余6条评论

1

Symbol构造函数有一个特殊的属性Symbol.iterator,其值是一个类似于“概念性”属性名“@@iterator”的东西。因此,您可以像这样为对象创建一个迭代器方法:

object[Symbol.iterator] = function* () {
  // do something that makes sense
};

您也可以通过以下方式测试某个对象是否为迭代器

if (Symbol.iterator in object)

现在,还要检查它是否是一个函数。这些迭代器函数(Symbol.iterator属性的值)是生成器函数(不是我在示例中编辑的*)。因此,您可以通过首先调用函数并保存返回的对象,然后调用.next()来启动它们并获取值。这将为您提供一个带有valuedone属性的对象。
或者,您可以让for ... of或"spread" ...运算符担心迭代器函数。

那么,这就回答了如何测试某个东西是否是官方可迭代的问题。但是,那么你该如何以最常规的方式进行迭代呢?你只需要使用“ for/of”吗? - jfriend00
@jfriend00 当然可以使用 for ... of,或者你可以使用 Array.from() 或解构赋值将其转换为数组。 - Pointy
还要注意的是,迭代器函数应该是一个 function* 函数,但我并不完全确定我真正理解了它。 - Pointy
2
function*是创建返回迭代器对象的函数的一种方式,但是创建一个返回符合迭代器对象标准的普通函数也可以。 - loganfsmyth
@loganfsmyth 好的,当然,那很有道理。 - Pointy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接