为什么对象在 JavaScript 中不可迭代?

IT技术 javascript arrays iterator ecmascript-6 ecmascript-5
2021-02-10 22:09:35

为什么默认情况下对象不可迭代?

我总是看到与迭代对象相关的问题,常见的解决方案是迭代对象的属性并以这种方式访问​​对象内的值。这似乎很常见,以至于我想知道为什么对象本身不可迭代。

for...of默认情况下,像 ES6这样的语句很适合用于对象。因为这些功能仅适用于不包含{}对象的特殊“可迭代对象” ,所以我们必须通过一些障碍来使这项功能适用于我们想要使用它的对象。

for...of 语句创建一个循环迭代可迭代对象 (包括 Array、Map、Set、arguments 对象等)...

例如使用 ES6生成器函数

var example = {a: {e: 'one', f: 'two'}, b: {g: 'three'}, c: {h: 'four', i: 'five'}};

function* entries(obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
}

for (let [key, value] of entries(example)) {
  console.log(key);
  console.log(value);
  for (let [key, value] of entries(value)) {
    console.log(key);
    console.log(value);
  }
}

当我在 Firefox(支持ES6)中运行代码时,上面按照我期望的顺序正确记录数据

hacky for...of 的输出

默认情况下,{}对象不可迭代,但为什么呢?缺点是否会超过对象可迭代的潜在好处?与此相关的问题是什么?

此外,由于{}对象不同于“类数组”集合和“可迭代对象”,例如NodeListHtmlCollection、 和arguments,因此它们无法转换为数组。

例如:

var argumentsArray = Array.prototype.slice.call(arguments);

或与 Array 方法一起使用:

Array.prototype.forEach.call(nodeList, function (element) {}).

除了我上面的问题,我很想看到一个关于如何将{}对象变成可迭代的工作示例,尤其是那些提到[Symbol.iterator]. 这应该允许这些新的{}“可迭代对象”使用像for...of. 另外,我想知道使对象可迭代是否允许将它们转换为数组。

我尝试了下面的代码,但我得到了一个TypeError: can't convert undefined to object.

var example = {a: {e: 'one', f: 'two'}, b: {g: 'three'}, c: {h: 'four', i: 'five'}};

// I want to be able to use "for...of" for the "example" object.
// I also want to be able to convert the "example" object into an Array.
example[Symbol.iterator] = function* (obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
};

for (let [key, value] of example) { console.log(value); } // error
console.log([...example]); // error
6个回答

我会试试这个。请注意,我不隶属于 ECMA,并且无法了解他们的决策过程,因此我无法明确说明他们为什么做或没有做任何事情。但是,我会陈述我的假设并尽力而为。

1. 为什么for...of首先要添加一个结构?

JavaScript 已经包含一个for...in可用于迭代对象属性的构造。然而,它并不是真正的 forEach 循环,因为它枚举了一个对象的所有属性,并且往往只在简单的情况下可预测地工作。

它在更复杂的情况下会崩溃(包括数组,在这种情况下,它的使用往往不鼓励或被正确使用for...in数组所需的保护措施彻底混淆)。您可以通过使用(除其他外)来解决这个问题,但这有点笨拙和不雅。 hasOwnProperty

因此,我的假设是for...of添加构造是为了解决与该for...in构造相关的缺陷,并在迭代事物时提供更大的实用性和灵活性。人们倾向于将循环视为通常可以应用于任何集合并在任何可能的上下文中产生合理结果for...inforEach循环,但事实并非如此。for...of循环修复了。

我还假设现有的 ES5 代码在 ES6 下运行并产生与在 ES5 下相同的结果很重要,因此不能对for...in构造的行为进行重大更改

2.如何for...of运作?

参考文档是对于这部分是有用的。具体来说,iterable如果一个对象定义了Symbol.iterator属性,那么它就会被考虑

属性定义应该是一个函数,它返回集合中的项目,一个,一个,一个,并设置一个标志,指示是否还有更多的项目要获取。一些对象类型提供了预定义的实现,并且使用for...of简单的委托给迭代器函数是相对清晰的

这种方法很有用,因为它可以非常简单地提供您自己的迭代器。我可能会说这种方法可能会带来实际问题,因为它依赖于定义以前没有的属性,除非我可以说情况并非如此,因为除非您故意去寻找它,否则基本上忽略了新属性(即它不会for...in作为键出现在循环中,等等)。所以事实并非如此。

撇开实际的非问题不谈,以新的预定义属性开始所有对象或隐含地说“每个对象都是一个集合”可能在概念上被认为是有争议的。

3. 为什么对象默认iterable使用for...of

我的猜测是,这是以下各项的组合:

  1. iterable默认所有对象可能被认为是不可接受的,因为它添加了一个以前没有的属性,或者因为对象不是(必然)集合。正如 Felix 所指出的,“迭代一个函数或一个正则表达式对象是什么意思”?
  2. 简单的对象已经可以使用 进行迭代for...in,目前尚不清楚内置迭代器实现与现有for...in行为有何不同/更好因此,即使 #1 是错误的并且添加属性是可以接受的,它也可能不被视为有用
  3. 想要创建对象的iterable用户可以通过定义Symbol.iterator属性轻松实现
  4. 所述ES6规范还提供了一个地图类型,它 iterable通过缺省,并且使用普通的对象作为具有其他的一些小的优点Map

参考文档中甚至为#3 提供了一个示例:

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

for (var value of myIterable) {
    console.log(value);
}

鉴于对象可以很容易地创建iterable,它们已经可以使用 进行迭代for...in,并且对于默认对象迭代器应该做什么(如果它所做的意味着与所做的有所不同for...in可能没有明确的共识,这似乎是合理的足以使iterable默认情况下不制作对象

请注意,您的示例代码可以使用for...in以下方法重写

for (let levelOneKey in object) {
    console.log(levelOneKey);         //  "example"
    console.log(object[levelOneKey]); // {"random":"nest","another":"thing"}

    var levelTwoObj = object[levelOneKey];
    for (let levelTwoKey in levelTwoObj ) {
        console.log(levelTwoKey);   // "random"
        console.log(levelTwoObj[levelTwoKey]); // "nest"
    }
}

...或者您也可以iterable通过执行以下操作以您想要的方式创建对象(或者您可以通过分配来创建所有对象):iterableObject.prototype[Symbol.iterator]

obj = { 
    a: '1', 
    b: { something: 'else' }, 
    c: 4, 
    d: { nested: { nestedAgain: true }}
};

obj[Symbol.iterator] = function() {
    var keys = [];
    var ref = this;
    for (var key in this) {
        //note:  can do hasOwnProperty() here, etc.
        keys.push(key);
    }

    return {
        next: function() {
            if (this._keys && this._obj && this._index < this._keys.length) {
                var key = this._keys[this._index];
                this._index++;
                return { key: key, value: this._obj[key], done: false };
            } else {
                return { done: true };
            }
        },
        _index: 0,
        _keys: keys,
        _obj: ref
    };
};

你可以在这里玩(至少在 Chrome 中):http : //jsfiddle.net/rncr3ppz/5/

编辑

针对您更新的问题,是的,可以iterable使用ES6 中展开运算符将 an 转换为数组

但是,这似乎还不能在 Chrome 中工作,或者至少我无法在我的 jsFiddle 中使用它。理论上它应该很简单:

var array = [...myIterable];
@Bergi - 因为我在文档中没有看到(并且没有看到这里描述的属性)。尽管支持明确定义迭代器的一个论点是,如果需要,它可以很容易地强制执行特定的迭代顺序。但是,如果迭代顺序不重要(或者如果默认顺序很好)并且单行快捷方式有效,则没有理由不采用更简洁的方法。
2021-03-14 22:09:35
oop,[[enumerate]]不是众所周知的符号(@@enumerate)而是内部方法。我必须是obj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
2021-03-14 22:09:35
为什么不只是obj[Symbol.iterator] = obj[Symbol.enumerate]在你的最后一个例子中
2021-03-22 22:09:35
当讨论的实际过程有据可查时,所有这些猜测又有什么用呢?您会说“因此我的假设是添加 for...of 构造以解决与 for...in 构造相关的缺陷”,这很奇怪。不。添加它是为了支持迭代任何东西的通用方法,并且是广泛的新功能集的一部分,包括可迭代对象本身、生成器以及映射和集合。它并不意味着替换或升级到for...in,它有不同的目的——遍历对象属性
2021-03-27 22:09:35
再次强调并不是每个对象都是一个集合的好点子。对象已经被这样使用了很长时间,因为它非常方便,但最终,它们并不是真正的集合。这就是我们Map现在所拥有的。
2021-04-12 22:09:35

我想问题应该是“为什么没有内置对象迭代?

为对象本身添加可迭代性可能会产生意想不到的后果,不,没有办法保证顺序,但编写迭代器就像这样简单

function* iterate_object(o) {
    var keys = Object.keys(o);
    for (var i=0; i<keys.length; i++) {
        yield [keys[i], o[keys[i]]];
    }
}

然后

for (var [key, val] of iterate_object({a: 1, b: 2})) {
    console.log(key, val);
}

a 1
b 2
谢谢虎三郎。我已经修改了我的问题。我很想看到一个例子[Symbol.iterator],如果你可以扩展那些意想不到的后果。
2021-04-11 22:09:35

Object出于很好的理由,不要在 Javascript 中实现迭代协议。在 JavaScript 中有两个级别可以迭代对象属性:

  • 程序级别
  • 数据层

程序级迭代

当您在程序级别迭代对象时,您会检查程序结构的一部分。这是一个反射操作。让我们用一个数组类型来说明这个语句,它通常在数据级别迭代:

const xs = [1,2,3];
xs.f = function f() {};

for (let i in xs) console.log(xs[i]); // logs `f` as well

我们刚刚检查了xs. 由于数组存储数据序列,我们通常只对数据级别感兴趣。for..in在大多数情况下,显然与数组和其他“面向数据”结构没有任何意义。这就是 ES2015 引入for..of可迭代协议的原因。

数据级迭代

这是否意味着我们可以通过区分函数和原始类型来简单地区分数据和程序级别?不,因为函数也可以是 Javascript 中的数据:

  • Array.prototype.sort 例如期望一个函数执行某种排序算法
  • Thunk 之类() => 1 + 2的只是懒惰评估值的功能包装器

除了原始值也可以表示程序级别:

  • [].length例如 is a Numberbut 代表一个数组的长度,因此属于程序域

这意味着我们不能仅仅通过检查类型来区分程序和数据级别。


重要的是要了解普通旧 Javascript 对象的迭代协议的实现将依赖于数据级别。但正如我们刚刚看到的,数据和程序级迭代之间的可靠区分是不可能的。

对于Arrays,这种区别是微不足道的:每个具有类似整数的键的元素都是一个数据元素。Objects 有一个等效的功能:enumerable描述符。但是,依靠这个真的可取吗?我相信不是!enumerable描述符的含义太模糊了。

结论

没有有意义的方法来实现对象的迭代协议,因为不是每个对象都是一个集合。

如果默认情况下对象属性是可迭代的,那么程序和数据级别就会混淆。由于在Javascript中每个复合类型是基于原生的对象,这将适用于ArrayMap为好。

for..in, Object.keys,Reflect.ownKeys等可用于反射和数据迭代,通常不可能有明确的区别。如果你不小心,你很快就会遇到元编程和奇怪的依赖关系。Map抽象数据类型有效地结束程序和数据电平的混为一谈。我相信Map是 ES2015 中最重要的成就,即使Promises 更令人兴奋。

+1,我认为“没有有意义的方法来实现对象的迭代协议,因为并非每个对象都是一个集合。” 总结。
2021-03-21 22:09:35
实际上,每个对象都是一个集合,集合是否一致并不是由语言来决定的。数组和地图也可以收集不相关的值。关键是您可以迭代任何对象的键,而不管它们的用途,因此您离迭代它们的值仅一步之遥。如果您谈论的是一种静态类型数组(或任何其他集合)值的语言,您可以谈论此类限制,但不能谈论 JavaScript。
2021-03-21 22:09:35
我不认为这是一个很好的论据。如果你的对象不是一个集合,你为什么要循环遍历它?并非每个对象都是集合并不重要,因为您不会尝试迭代不是集合的对象。
2021-03-28 22:09:35
认为每个对象都不是集合的论点是没有意义的。您假设迭代器只有一个目的(迭代集合)。对象上的默认迭代器将是对象属性的迭代器,无论这些属性代表什么(无论是集合还是其他东西)。正如 Manngo 所说,如果你的对象不代表一个集合,那么程序员就不能把它当作一个集合来对待。也许他们只是想迭代对象上的属性以获得一些调试输出?除了收藏之外,还有很多其他的原因。
2021-04-10 22:09:35

我也被这个问题困扰。

然后我想出了一个使用的想法Object.entries({...}),它返回Array一个Iterable

此外,Axel Rauschmayer 博士对此发表了出色的回答。请参阅为什么普通对象不可迭代

这太棒了。正是我要找的。Object.entries({...}).forEach(function() {...}); 完美运行。
2021-03-16 22:09:35

您可以轻松地使所有对象全局可迭代:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});
不要全局地向本机对象添加方法。这是一个可怕的想法,它会咬你,以及任何使用你的代码的人。
2021-04-05 22:09:35