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

117

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

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

像 ES6 中的 for...of 这样的语句默认情况下不支持对象,只适用于特殊“可迭代对象”,而 {} 对象不在其中,因此我们必须费尽周折才能使其适用于我们想要使用它的对象。

for...of 语句创建一个循环,用于迭代可迭代对象(包括数组、映射、集合、参数对象等)...

例如,使用 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);
  }
}

当我在支持ES6的Firefox中运行代码时,以上内容能够按照我期望的顺序正确记录数据:

hacky for...of的输出

默认情况下,{}对象不可迭代,但为什么?对象可迭代的潜在好处是否会被劣势所超越?与此相关的问题是什么?

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

例如:

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

或者使用数组方法:

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

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

我尝试了下面的代码,但是我得到了一个TypeError:无法将未定义转换为对象

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 属性的对象都是可迭代的。因此,您只需要实现该属性即可。为什么对象不可迭代的一个可能的解释是,这将意味着一切都是可迭代的,因为一切都是对象(当然除了原始类型)。但是,遍历函数或正则表达式对象意味着什么? - Felix Kling
7
你的实际问题是什么?为什么ECMA要做出这些决定? - Steve Bennett
3
由于对象的属性没有保证的顺序,我想知道这是否违反了可迭代对象的定义,因为你期望它们具有可预测的顺序? - jfriend00
2
要得到一个权威的答案,关于“为什么”,你应该在 https://esdiscuss.org/ 上提问。 - Felix Kling
1
@FelixKling - 这篇文章是关于ES6的吗?你应该编辑一下,说明你所讨论的版本,因为“即将推出的ECMAScript版本”随着时间的推移不太可行。 - jfriend00
显示剩余13条评论
7个回答

57
我会尝试一下。请注意,我与ECMA无关,也没有他们决策过程的可见性,因此我不能确定地说他们为什么做或不做某事。但是,我将说明我的假设并尽力而为。
1.首先,为什么要添加一个for...of结构?
JavaScript已经包括一个for...in结构,可用于迭代对象的属性。然而,它不是真正的forEach循环,因为它枚举对象上的所有属性,并且在简单情况下只能可靠地工作。在更复杂的情况下(包括数组,在那里使用它往往被防护所需彻底混淆),它会崩溃。您可以通过使用hasOwnProperty(等其他方法)来解决这个问题,但这有点笨拙和不优雅。
因此,我的假设是for...of结构被添加以解决与for...in结构相关的缺陷,并在迭代事物时提供更大的效用和灵活性。人们倾向于将for...in视为可以通常应用于任何集合并在任何可能的情况下产生明智结果的forEach循环,但事实并非如此。 for...of循环修复了这个问题。
我还假设现有的ES5代码在ES6下运行并产生与在ES5下相同的结果很重要,因此不能对for...in结构的行为进行破坏性改变。
2. for...of如何工作? 参考文档对于这部分非常有用。具体而言,如果对象定义了Symbol.iterator属性,则认为它是可迭代的。
属性定义应该是一个函数,该函数逐个返回集合中的项,并设置一个指示是否有更多项需要获取的标志。某些对象类型提供了预定义的实现,使用for...of只是将其委托给迭代器函数。
这种方法很有用,因为它使提供自己的迭代器变得非常简单。我可能会说,由于它依赖于定义先前不存在的属性,因此这种方法可能会出现实际问题,但据我所知,除非你刻意寻找它(即它不会在for...in循环中作为键等出现),否则新属性基本上会被忽略。所以这不是问题。
除了实际问题之外,从概念上讲,将所有对象都默认设置为具有新的预定义属性,或者暗示“每个对象都是一个集合”,可能会引起争议。
3. 为什么默认情况下对象不使用for...of可迭代?

我猜测这是以下内容的结合:

  1. 默认情况下使所有对象可迭代可能被认为是不可接受的,因为它增加了以前不存在的属性,或者因为对象不是(必然)一个集合。正如Felix指出的那样,“在函数或正则表达式对象上迭代意味着什么?”
  2. 简单对象可以使用for...in进行迭代,而且不清楚内置迭代器实现与现有的for...in行为有何不同/更好。因此,即使#1是错误的,并且添加属性是可以接受的,它也可能不被视为有用。
  3. 想要使其对象可迭代的用户可以通过定义Symbol.iterator属性来轻松实现。
  4. ES6规范还提供了Map类型,默认情况下是iterable,并且与将普通对象用作Map相比具有一些小优势。
即使在参考文档中,也提供了有关第三点的示例:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

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

鉴于对象可以轻松地成为“可迭代”对象,它们已经可以使用“for...in”进行迭代,并且可能没有明确的协议规定默认对象迭代器应该执行什么操作(如果其操作与“for...in”的操作有所不同),因此,默认情况下对象似乎并没有被定义为“可迭代”的。请注意,您的示例代码可以使用“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,也可以通过将其分配给 Object.prototype[Symbol.iterator] 来使 所有 对象都成为 iterable

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/ 编辑 针对您更新的问题,是可能将可迭代对象转换为数组,在ES6中使用扩展运算符spread operator
然而,在Chrome中似乎还不能正常工作,或者至少我无法在我的jsFiddle中使其正常工作。理论上应该很简单:
var array = [...myIterable];

为什么不在你的最后一个例子中只做 obj[Symbol.iterator] = obj[Symbol.enumerate] 呢? - Bergi
@Bergi - 因为我在文档中没有看到那个(并且在这里也没有看到该属性的描述 (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol))。虽然明确定义迭代器的一个优点是易于强制执行特定的迭代顺序,如果需要的话。但是,如果迭代顺序不重要(或者如果默认顺序可以),并且一行快捷方式可行,则几乎没有理由不采用更简洁的方法。 - aroth
糟糕,[[enumerate]] 不是一个众所周知的符号 (@@enumerate),而是一个内部方法。我需要 obj[Symbol.iterator] = function(){ return Reflect.enumerate(this) } - Bergi
所有这些猜测有什么用呢,当实际的讨论过程已经被充分记录下来?你说“因此我的假设是for...of结构被添加以解决与for...in结构相关的缺陷。”这非常奇怪。它被添加是为了支持一种通用的迭代方式,是一系列新功能的一部分,包括可迭代对象本身、生成器和映射和集合。它几乎不意味着要替换或升级for...in,后者具有不同的目的——遍历对象的属性 - user663031
@torazaburo - “它被添加是为了支持通用的迭代方式。” 是的,但是为什么?问题在于,这正是许多人认为for...in应该做的事情,这让他们非常沮丧。我很难相信ES6背后的人不知道这个事实,它引起的问题,以前(有时是已弃用) 添加类似功能的尝试,以及语言对一个_可工作的_ forEach 循环的迫切需求。 - aroth
3
再次强调并很好地指出,并不是每个对象都是一个集合。长期以来,对象一直被用作集合,因为它非常方便,但最终它们并不真正是集合。这就是现在我们有 Map 的原因。 - Felix Kling

11

我也被这个问题困扰。

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

此外,Axel Rauschmayer博士在此方面发表了一篇优秀的答案。 请参见为什么普通对象不可迭代


1
这太棒了。正是我在寻找的东西。Object.entries({...}).forEach(function() {...}); 完美运行。 - keyboardface

11

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期望一个函数执行特定的排序算法
  • () => 1 + 2这样的惰性求值值的函数包装器仅仅是thunk

此外,原始值也可以表示程序级别:

  • [].length是一个Number,但表示数组的长度,因此属于程序域

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


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

对于Array,这个区分是微不足道的:具有类似整数键的元素都是数据元素。 Object也有一个类似的功能:可枚举描述符。但真的建议依赖于它吗?我认为不是!可枚举描述符的含义太模糊了。

结论

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

如果默认情况下对象属性可迭代,程序和数据级别将被混淆。由于Javascript中的每种复合类型都基于普通对象,因此这也适用于ArrayMap

for..inObject.keysReflect.ownKeys等可以用于反射和数据迭代,但通常无法明确区分。如果不小心,您很快就会使用元编程和奇怪的依赖项。 Map抽象数据类型有效地结束了程序级别和数据级别的混淆。我认为Map是ES2015中最重要的成就,即使Promise更加令人兴奋。


4
+1,我认为“对于对象来说,没有有意义的方法来实现迭代协议,因为并不是每个对象都是一个集合”这句话概括了这个问题。 - Charlie Schliesser
2
我认为这不是一个好的论点。如果你的对象不是一个集合,为什么要尝试循环它?并不重要的是,并非每个对象都是一个集合,因为你不会尝试迭代那些不是集合的对象。 - B T
实际上,每个对象都是一个集合,而语言不能决定集合是否连贯。数组和映射还可以收集不相关的值。重点在于,无论对象键的用途如何,您都可以迭代它们,因此您距离迭代其值只有一步之遥。如果您谈论的是静态类型数组(或任何其他收集)值的语言,则可以谈论此类限制,但不适用于JavaScript。 - Manngo
那个说每个对象都不是集合的论点没有意义。你假设迭代器只有一个目的(迭代集合)。对象上的默认迭代器将是对象属性的迭代器,无论这些属性表示什么(无论是集合还是其他东西)。正如Manngo所说,如果您的对象不表示集合,则由程序员决定不将其视为集合。也许他们只想迭代对象上的属性以进行一些调试输出?除了集合之外,还有很多其他原因。 - jfriend00

10
我想问题应该是“为什么没有内置的对象迭代器?”将可迭代性添加到对象本身可能会产生意想不到的后果,而且不能保证顺序,但编写迭代器就像这样简单:
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]];
            }
        }
    }
});

6
不要在本地对象中全局添加方法。这是一个可怕的想法,会让你和使用你代码的任何人都受到影响。请注意不改变原意,尽可能使语言通俗易懂。 - B T

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

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

而且,因为每个人都有权发表意见,我不明白为什么对象不已经是可迭代的。如果您可以像上面那样填充它们,或者使用for ... in,那么我看不出有什么简单的争论。

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


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