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


87

为什么默认情况下无法迭代对象?

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

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的输出...

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

此外,由于{}对象是从“阵列状”集合和不同的“可迭代对象”,如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

1
任何具有Symbol.iterator属性的东西都是可迭代的。因此,您只需要实现该属性即可。关于对象为何不可迭代的一种可能的解释可能是,这暗示着一切都是可迭代的,因为一切都是对象(当然,原语除外)。但是,遍历函数或正则表达式对象意味着什么?
菲利克斯·克林

7
您在这里实际的问题是什么?ECMA为什么要做出决定?
史蒂夫·贝内特

3
由于对象没有确定其属性的顺序,因此我想知道这是否与您希望具有可预测顺序的可迭代对象的定义相违背?
jfriend00

2
要获得“为什么”的权威答案,您应该在esdiscuss.org上提问
Felix Kling

1
@FelixKling-关于ES6的帖子吗?您可能应该对其进行编辑,以说出您在说什么版本,因为随着时间的推移,“即将发布的ECMAScript版本”无法很好地运行。
jfriend00

Answers:


42

我会尝试的。请注意,我不隶属于ECMA,也不了解他们的决策过程,因此我无法确切地说出他们为什么做或没有做任何事情。但是,我会陈述自己的假设并尽我最大的努力。

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

JavaScript已经包含一个for...in可用于迭代对象属性的构造。但是,它实际上不是forEach循环,因为它枚举了对象上的所有属性,并且仅在可预见的情况下才能正常工作。

在更复杂的情况下(包括在数组中,使用正确的数组所需的保护措施不鼓励或完全混淆它的使用),它会崩溃。您可以使用(除其他方法外)解决此问题,但这有点笨拙且不雅致。 for...inhasOwnProperty

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

我还认为,对于现有的ES5代码来说,在ES6下运行并产生与ES5下相同的结果非常重要,因此无法对for...in结构的行为进行重大更改。

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

参考文档是对于这部分是有用的。具体来说,如果对象iterable定义了Symbol.iterator属性,则将其视为对象。

property-definition应该是一个函数,该函数一个接一个地返回集合中的项目,并设置一个标志,该标志指示是否还有更多要提取的项目。为某些对象类型提供了预定义的实现,并且很显然,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中的散布运算符将转换为数组。

但是,这似乎在Chrome中尚不可用,或者至少我无法在jsFiddle中使用它。从理论上讲,它应该很简单:

var array = [...myIterable];

为什么不只是obj[Symbol.iterator] = obj[Symbol.enumerate]在最后一个示例中做呢?
Bergi

@Bergi-因为我没有在文档中看到它(也没有看到此处描述的属性)。尽管支持显式定义迭代器的一个论点是,如果需要的话,它可以很容易地强制执行特定的迭代顺序。如果迭代顺序不重要(或者默认顺序很好)并且单行快捷方式有效,则没有理由不采用更简洁的方法。
aroth 2015年

糟糕,[[enumerate]]这不是众所周知的符号(@@ enumerate),而是一种内部方法。我一定会是obj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi 2015年

充分记录讨论的实际过程时,所有这些猜测有什么用?您会说“因此很奇怪,因此我的假设是要添加for ... of构造,以解决与for ... in构造相关的缺陷。” 否。它是为了支持一种通用的迭代方法而添加的,它是一系列新功能的一部分,其中包括可迭代对象本身,生成器以及地图和集合。它几乎不是要替换或升级为for...in,其目的是不同的-遍历对象的属性

2
再次强调并不是每个对象都是一个集合的好处。这样使用对象已经很长时间了,因为它非常方便,但是最终它们并不是真正的集合。那就是我们Map现在所拥有的。
Felix Kling 2015年

9

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例如a,Number但代表数组的长度,因此属于程序域

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


重要的是要理解,普通的旧Javascript对象的迭代协议的实现将取决于数据级别。但是,正如我们已经看到的那样,不可能在数据和程序级迭代之间进行可靠的区分。

使用Arrays时,这种区别是微不足道的:每个具有类似整数的键的元素都是数据元素。Object具有等效功能:enumerable描述符。但是,依靠它真的明智吗?我相信不是!enumerable描述符的含义太模糊。

结论

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

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

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


3
+1,我认为“没有实现对象迭代协议的有意义的方法,因为不是每个对象都是一个集合。” 总结。
Charlie Schliesser

1
我认为这不是一个很好的论点。如果您的对象不是集合,为什么要遍历它?并非每个对象都不是一个集合都没关系,因为您不会尝试遍历不是的对象。
BT

实际上,每个对象都是一个集合,并且由语言来决定集合是否一致。数组和地图也可以收集不相关的值。关键是,无论对象的用途如何,都可以对其进行迭代,因此距离迭代其值仅一步之遥。如果您正在谈论一种静态地类型化数组(或任何其他集合)值的语言,则可以谈论这种限制,而不是JavaScript。
曼戈

关于每个对象都不是集合的说法没有道理。您假设迭代器只有一个目的(迭代集合)。对象上的默认迭代器将是对象属性的迭代器,无论这些属性代表什么(无论是集合还是其他)。正如Manngo所说,如果您的对象不代表一个集合,那么程序员就不能将其视为一个集合。也许他们只是想迭代对象的属性以获得某些调试输出?除集合外,还有许多其他原因。
jfriend00

8

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

可以想象,给对象本身添加可迭代性可能会带来意想不到的后果,没有,没有办法保证顺序,但是编写迭代器非常简单

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

1
感谢torazaburo。我已经修改了我的问题。我希望看到一个示例[Symbol.iterator],以及是否可以扩展这些意外的结果。
boombox

4

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

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

3
不要在全局对象上添加方法。这是一个可怕的主意,会让您和任何使用您的代码的人都被咬。
BT

2

这是最新的方法(在chrome canary中有效)

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

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

是的entries,现在这就是Object的一部分的方法:)

编辑

在深入研究之后,您似乎可以执行以下操作

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

所以你现在可以做

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

编辑2

回到您的原始示例,如果您想使用上述原型方法,则需要这样

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

2

这个问题我也很困扰。

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

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


0

从技术上讲,这不是为什么的答案但是我根据BT的评论对上面的Jack Slocum的答案进行了修改,使其可以用于使对象可迭代。

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

并不像它应该的那样方便,但是它是可行的,尤其是当您有多个对象时:

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

而且,因为每个人都有权发表意见,所以我也无法理解为什么Objects也不能迭代。如果您可以像上面那样填充或使用它们,for … in那么我看不到一个简单的参数。

一个可能的建议是,可迭代的是对象的类型,因此,有可能将可迭代限制为对象的子集,以防万一其他对象在尝试中爆炸。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.