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

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

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

默认情况下,对对象使用 ES6for...of这样的语句会更好。因为这些特性只适用于不包含 {}对象的特殊“可迭代对象”,所以我们必须克服一些困难才能使这些特性适用于我们想要使用它的对象。

语句的 for... of 创建一个循环,迭代 < strong > 迭代对象 (包括数组、映射、集合、参数对象等)。

例如,使用 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)时期望的顺序正确地记录数据:

output of hacky for...of

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

此外,由于 {}对象不同于“类数组”集合和“可迭代对象”(如 NodeListHtmlCollectionarguments) ,因此不能将它们转换为数组。

例如:

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

或与 Array 方法一起使用:

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

除了上面提到的问题,我还想看一个关于如何将 {}对象转换为可迭代对象的工作示例,特别是那些提到过 [Symbol.iterator]的人。这应该允许这些新的 {}“可迭代对象”使用像 for...of这样的语句。另外,我想知道使对象可迭代是否允许将它们转换为 Array。

我试了下面的代码,但我得到了一个 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
197846 次浏览

I guess the question should be "why is there no built-in object iteration?

Adding iterability to objects themselves could conceivably have unintended consequences, and no, there is no way to guarantee order, but writing an iterator is as simple as

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

Then

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


a 1
b 2

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

Objects don't implement the iteration protocols in Javascript for very good reasons. There are two levels at which object properties can be iterated over in JavaScript:

  • the program level
  • the data level

Program Level Iteration

When you iterate over an object at the program level you examine a portion of the structure of your program. It is a reflective operation. Let's illustrate this statement with an array type, which is usually iterated over at the data level:

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


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

We just examined the program level of xs. Since arrays store data sequences, we are regularly interested in the data level only. for..in evidently makes no sense in connection with arrays and other "data-oriented" structures in most cases. That is the reason why ES2015 has introduced for..of and the iterable protocol.

Data Level Iteration

Does that mean that we can simply distinguish the data from the program level by distinguishing functions from primitive types? No, because functions can also be data in Javascript:

  • Array.prototype.sort for instance expects a function to perform a certain sort algorithm
  • Thunks like () => 1 + 2 are just functional wrappers for lazily evaluated values

Besides primitive values can represent the program level as well:

  • [].length for instance is a Number but represents the length of an array and thus belongs to the program domain

That means that we can't distinguish the program and data level by merely checking types.


It is important to understand that the implementation of the iteration protocols for plain old Javascript objects would rely on the data level. But as we've just seen, a reliable distinction between data and program level iteration is not possible.

With Arrays this distinction is trivial: Every element with an integer-like key is a data element. Objects have an equivalent feature: The enumerable descriptor. But is it really advisable to rely on this? I believe it is not! The meaning of the enumerable descriptor is too blurry.

Conclusion

There is no meaningful way to implement the iteration protocols for objects, because not every object is a collection.

If object properties were iterable by default, program and data level were mixed-up. Since every composite type in Javascript is based on plain objects this would apply for Array and Map as well.

for..in, Object.keys, Reflect.ownKeys etc. can be used for both reflection and data iteration, a clear distinction is regularly not possible. If you're not careful, you end up quickly with meta programming and weird dependencies. The Map abstract data type effectively ends the conflating of program and data level. I believe Map is the most significant achievement in ES2015, even if Promises are much more exciting.

This is the latest approach (which works in chrome canary)

var files = {
'/root': {type: 'directory'},
'/root/example.txt': {type: 'file'}
};


for (let [key, {type}] of Object.entries(files)) {
console.log(type);
}

Yes entries is now a method thats part of Object :)

edit

After looking more into it, it seems you could do the following

Object.prototype[Symbol.iterator] = function * () {
for (const [key, value] of Object.entries(this)) {
yield {key, value}; // or [key, value]
}
};

so you can now do this

for (const {key, value:{type}} of files) {
console.log(key, type);
}

edit2

Back to your original example, if you wanted to use the above prototype method it would like like this

for (const {key, value:item1} of example) {
console.log(key);
console.log(item1);
for (const {key, value:item2} of item1) {
console.log(key);
console.log(item2);
}
}

You can easily make all objects iterable globally:

Object.defineProperty(Object.prototype, Symbol.iterator, {
enumerable: false,
value: function * (){
for(let key in this){
if(this.hasOwnProperty(key)){
yield [key, this[key]];
}
}
}
});

Technically, this is not an answer to the question why? but I have adapted Jack Slocum’s answer above in light of BT’s comments to something which can be used to make an Object iterable.

var iterableProperties={
enumerable: false,
value: function * () {
for(let key in this) if(this.hasOwnProperty(key)) yield this[key];
}
};


var fruit={
'a': 'apple',
'b': 'banana',
'c': 'cherry'
};
Object.defineProperty(fruit,Symbol.iterator,iterableProperties);
for(let v of fruit) console.log(v);

Not quite as convenient as it should have been, but it’s workable, especially if you have multiple objects:

var instruments={
'a': 'accordion',
'b': 'banjo',
'c': 'cor anglais'
};
Object.defineProperty(instruments,Symbol.iterator,iterableProperties);
for(let v of instruments) console.log(v);

And, because every one is entitled to an opinion, I can’t see why Objects are not already iterable either. If you can polyfill them as above, or use for … in then I can’t see a simple argument.

One possible suggestion is that what is iterable is a type of object, so it is possible that iterable has been limited to a subset of objects just in case some other objects explode in the attempt.

I was also bothered with this question.

Then I came up with an idea of using Object.entries({...}), it returns an Array which is an Iterable.

Also, Dr. Axel Rauschmayer posted an excellent answer on this. See Why plain objects are NOT iterable