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

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

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

类似 ES6 的语句 , HtmlCollectionarguments,它们不能转换成数组.

例如:

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

或与数组方法一起使用:

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

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

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

var example = {a: {e: '一', f: '二'}, b: {g: '三'}, c: {h: '四', i: '五'}};//我希望能够对example"对象使用for...of".//我还希望能够将示例"对象转换为数组.示例[Symbol.iterator] = function* (obj) {for (let key of Object.keys(obj)) {产量 [key, obj[key]];}};for (let [key, value] of example) { console.log(value);}//错误console.log([...example]);//错误

解决方案

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

1.为什么首先要添加 for...of 构造?

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

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

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

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

<强>2.for...of 是如何工作的?

参考文档对此部分很有用.具体来说,如果一个对象定义了 Symbol.iterator 属性,则该对象被视为 iterable.

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

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

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

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

我的猜测是这是以下的组合:

  1. 默认情况下使所有对象 iterable 可能被认为是不可接受的,因为它在以前没有属性的地方添加了一个属性,或者因为一个对象(不一定)不是一个集合.正如 Felix 所说,迭代函数或正则表达式对象意味着什么"?
  2. 简单的对象已经可以使用 for...in 进行迭代了,目前尚不清楚内置的迭代器实现与现有的 for... 相比有什么不同/更好. 行为.因此,即使 #1 是错误的并且添加该属性是可以接受的,它也可能不会被视为有用.
  3. 想要使他们的对象iterable 的用户可以通过定义Symbol.iterator 属性轻松地做到这一点.
  4. ES6 规范还提供了 地图 类型,默认情况下 是 iterable,并且与使用普通对象作为 Map 相比还有一些其他的小优势.

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

var myIterable = {};myIterable[Symbol.iterator] = function* () {产量1;产量2;产量 3;};for (var value of myIterable) {控制台.log(值);}

鉴于对象可以很容易地制成 iterable,它们已经可以使用 for...in 进行迭代,而且对于什么是默认值可能没有明确的协议对象迭代器应该做的(如果它所做的与 for...in 所做的有所不同),对象不是由 iterable默认.

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

for (let levelOneKey in object) {console.log(levelOneKey);// 例子"console.log(object[levelOneKey]);//{"random":"nest","another":"thing"}var levelTwoObj = object[levelOneKey];for (let levelTwoKey in levelTwoObj ) {console.log(levelTwoKey);//随机的"console.log(levelTwoObj[levelTwoKey]);//巢"}}

...或者您也可以通过执行以下操作以您想要的方式使您的对象 iterable (或者您可以使 all 对象 iterable 通过分配给 Object.prototype[Symbol.iterator] 代替):

obj = {一个:'1',b:{某事:'其他'},三:4,d:{嵌套:{nestedAgain:真}}};obj[Symbol.iterator] = function() {变量键 = [];var ref = 这个;for (var key in this) {//注意:可以在这里做hasOwnProperty(),等等.键.push(键);}返回 {下一个:函数(){if (this._keys && this._obj && this._index < this._keys.length) {var key = this._keys[this._index];this._index++;返回{键:键,值:this._obj [键],完成:假};} 别的 {返回{完成:真};}},_index: 0,_keys:键,_obj:参考};};

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

编辑

并且针对您更新的问题,是的,可以使用 iterable 转换为数组ES6 中的 US/docs/Web/JavaScript/Reference/Operators/Spread_operator" rel="noreferrer">扩展运算符.

但是,这似乎还没有在 Chrome 中运行,或者至少我无法让它在我的 jsFiddle 中运行.理论上它应该很简单:

var 数组 = [...myIterable];

Why are objects not iterable by default?

I see questions all the time related to iterating objects, the common solution being to iterate over an object's properties and accessing the values within an object that way. This seems so common that it makes me wonder why objects themselves aren't iterable.

Statements like the ES6 for...of would be nice to use for objects by default. Because these features are only available for special "iterable objects" which don't include {} objects, we have to go through hoops to make this work for objects we want to use it for.

The for...of statement creates a loop Iterating over iterable objects (including Array, Map, Set, arguments object and so on)...

For example using an ES6 generator function:

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);
  }
}

The above properly logs data in the order I expect it to when I run the code in Firefox (which supports ES6):

By default, {} objects are not iterable, but why? Would the disadvantages outweigh the potential benefits of objects being iterable? What are the issues associated with this?

In addition, because {} objects are different from "Array-like" collections and "iterable objects" such as NodeList, HtmlCollection, and arguments, they can't be converted into Arrays.

For example:

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

or be used with Array methods:

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

Besides the questions I have above, I would love to see a working example on how to make {} objects into iterables, especially from those who have mentioned the [Symbol.iterator]. This should allow these new {} "iterable objects" to use statements like for...of. Also, I wonder if making objects iterable allow them to be converted into Arrays.

I tried the below code, but I get a 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

解决方案

I'll give this a try. Note that I'm not affiliated with ECMA and have no visibility into their decision-making process, so I cannot definitively say why they have or have not done anything. However, I'll state my assumptions and take my best shot.

1. Why add a for...of construct in the first place?

JavaScript already includes a for...in construct that can be used to iterate the properties of an object. However, it's not really a forEach loop, as it enumerates all of the properties on an object and tends to only work predictably in simple cases.

It breaks down in more complex cases (including with arrays, where its use tends to be either discouraged or thoroughly obfuscated by the safeguards needed to for use for...in with an array correctly). You can work around that by using hasOwnProperty (among other things), but that's a bit clunky and inelegant.

So therefore my assumption is that the for...of construct is being added to address the deficiencies associated with the for...in construct, and provide greater utility and flexibility when iterating things. People tend to treat for...in as a forEach loop that can be generally applied to any collection and produce sane results in any possible context, but that's not what happens. The for...of loop fixes that.

I also assume that it's important for existing ES5 code to run under ES6 and produce the same result as it did under ES5, so breaking changes cannot be made, for instance, to the behavior of the for...in construct.

2. How does for...of work?

The reference documentation is useful for this part. Specifically, an object is considered iterable if it defines the Symbol.iterator property.

The property-definition should be a function that returns the items in the collection, one, by, one, and sets a flag indicating whether or not there are more items to fetch. Predefined implementations are provided for some object-types, and it's relatively clear that using for...of simply delegates to the iterator function.

This approach is useful, as it makes it very straightforward to provide your own iterators. I might say the approach could have presented practical issues due to its reliance upon defining a property where previously there was none, except from what I can tell that's not the case as the new property is essentially ignored unless you deliberately go looking for it (i.e. it will not present in for...in loops as a key, etc.). So that's not the case.

Practical non-issues aside, it may have been considered conceptually controversial to start all objects off with a new pre-defined property, or to implicitly say that "every object is a collection".

3. Why are objects not iterable using for...of by default?

My guess is that this is a combination of:

  1. Making all objects iterable by default may have been considered unacceptable because it adds a property where previously there was none, or because an object isn't (necessarily) a collection. As Felix notes, "what does it mean to iterate over a function or a regular expression object"?
  2. Simple objects can already be iterated using for...in, and it's not clear what a built-in iterator implementation could have done differently/better than the existing for...in behavior. So even if #1 is wrong and adding the property was acceptable, it may not have been seen as useful.
  3. Users who want to make their objects iterable can easily do so, by defining the Symbol.iterator property.
  4. The ES6 spec also provides a Map type, which is iterable by default and has some other small advantages over using a plain object as a Map.

There's even an example provided for #3 in the reference documentation:

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

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

Given that objects can easily be made iterable, that they can already be iterated using for...in, and that there's likely not clear agreement on what a default object iterator should do (if what it does is meant to be somehow different from what for...in does), it seems reasonable enough that objects were not made iterable by default.

Note that your example code can be rewritten using 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"
    }
}

...or you can also make your object iterable in the way you want by doing something like the following (or you can make all objects iterable by assigning to Object.prototype[Symbol.iterator] instead):

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
    };
};

You can play with that here (in Chrome, at lease): http://jsfiddle.net/rncr3ppz/5/

Edit

And in response to your updated question, yes, it is possible to convert an iterable to an array, using the spread operator in ES6.

However, this doesn't seem to be working in Chrome yet, or at least I cannot get it to work in my jsFiddle. In theory it should be as simple as:

var array = [...myIterable];

相关文章