为啥 JavaScript 中的对象不可迭代?

Posted

技术标签:

【中文标题】为啥 JavaScript 中的对象不可迭代?【英文标题】:Why are Objects not Iterable in JavaScript?为什么 JavaScript 中的对象不可迭代? 【发布时间】:2020-04-09 07:54:30 【问题描述】:

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

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

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

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

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

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

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

另外,由于对象不同于“类数组”集合和“可迭代对象”如NodeListhtmlCollectionarguments,它们不能转换成数组。

例如:

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: '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

【问题讨论】:

任何具有Symbol.iterator 属性的东西都是可迭代的。所以你只需要实现那个属性。对于为什么对象不可迭代的一个可能解释可能是这将暗示一切都是可迭代的,因为一切都是对象(当然除了原语)。但是,迭代函数或正则表达式对象是什么意思? 您的实际问题是什么?为什么 ECMA 会做出这样的决定? 由于对象没有保证其属性的顺序,我想知道这是否违反了您期望具有可预测顺序的可迭代对象的定义? 要获得“为什么”的权威答案,您应该询问esdiscuss.org @FelixKling - 那篇文章是关于 ES6 的吗?您可能应该编辑它以说明您正在谈论的版本,因为“即将推出的 ECMAScript 版本”随着时间的推移效果不佳。 【参考方案1】:

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

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

javascript 已经包含一个for...in 构造,可用于迭代对象的属性。但是,它是not really a forEach loop,因为它枚举了对象上的所有属性,并且往往只能在简单的情况下以可预测的方式工作。

它在更复杂的情况下会崩溃(包括数组,其中它的使用往往是 discouraged or thoroughly obfuscated 由将for...in 与数组一起使用所需的保障正确)。您可以通过使用hasOwnProperty(除其他外)来解决这个问题,但这有点笨拙和不雅。

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

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

2。 for...of 是如何工作的?

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

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

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

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

3.为什么对象默认不使用iterablefor...of

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

    默认设置所有对象iterable 可能被认为是不可接受的,因为它在以前没有属性的地方添加了一个属性,或者因为一个对象(不一定)不是一个集合。正如 Felix 所说,“迭代函数或正则表达式对象意味着什么”? 简单的对象已经可以使用for...in 进行迭代,而且还不清楚内置的迭代器实现与现有的for...in 行为有什么不同/更好。因此,即使 #1 是错误的并且添加该属性是可以接受的,它也可能不会被视为有用。 想要将对象设为iterable 的用户可以通过定义Symbol.iterator 属性轻松实现。 ES6 规范还提供了一个Map 类型,默认情况下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(或者您可以通过分配给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 将 iterable 转换为数组。

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

var array = [...myIterable];

【讨论】:

为什么不在你的最后一个例子中直接使用obj[Symbol.iterator] = obj[Symbol.enumerate] @Bergi - 因为我在文档中没有看到这一点(也没有看到 here 描述的那个属性)。尽管支持显式定义迭代器的一个论点是,如果需要的话,它可以很容易地强制执行特定的迭代顺序。如果迭代顺序不重要(或者如果默认顺序很好)并且单行快捷方式有效,那么没有理由不采用更简洁的方法。 糟糕,[[enumerate]] 不是众所周知的符号 (@@enumerate),而是一种内部方法。我必须是obj[Symbol.iterator] = function() return Reflect.enumerate(this) 当讨论的实际过程有据可查时,所有这些猜测有什么用?您会说“因此我的假设是添加 for...of 构造以解决与 for...in 构造相关的缺陷,这很奇怪。”不。它被添加来支持迭代任何东西的通用方法,并且是一系列广泛的新特性的一部分,包括可迭代对象本身、生成器以及地图和集合。它几乎不意味着替换或升级到 for...in,它有不同的目的——遍历对象的 属性 好点再次强调不是每个对象都是一个集合。对象已经使用了很长时间,因为它非常方便,但归根结底,它们并不是真正的集合。这就是我们现在拥有的Map【参考方案2】:

Objects 没有在 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 期望函数执行某种排序算法 像 () =&gt; 1 + 2 这样的 Thunk 只是延迟评估值的功能包装器

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

[].length 例如是 Number 但代表数组的长度,因此属于程序域

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


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

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

结论

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

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

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

【讨论】:

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

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

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

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

【讨论】:

感谢 torazaburo。我已经修改了我的问题。我很想看到一个使用[Symbol.iterator] 的例子,以及如果你能扩展这些意想不到的后果。【参考方案4】:

我也被这个问题困扰。

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

此外,Axel Rauschmayer 博士对此发表了出色的回答。 见Why plain objects are NOT iterable

【讨论】:

这太棒了。正是我想要的。 Object.entries(...).forEach(function() ... );完美运行。【参考方案5】:

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

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

【讨论】:

不要将方法全局添加到本机对象。这是一个可怕的想法,会咬你,以及任何使用你的代码的人。【参考方案6】:

这是最新的方法(适用于 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);
    

【讨论】:

【参考方案7】:

从技术上讲,这不是对为什么?问题的答案,但我已经根据 BT 的 cmets 将 Jack Slocum 的上述答案改编为可用于使 Object 可迭代的东西。

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,那么我看不到一个简单的论点。

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

【讨论】:

以上是关于为啥 JavaScript 中的对象不可迭代?的主要内容,如果未能解决你的问题,请参考以下文章

Django - TypeError - NoneType' 对象不可迭代 - 不明白为啥 Object 被识别为 None

为啥 Javascript 生成器既是迭代器又是可迭代对象?

TypeError:“参考”对象在 javascript 中不可迭代

为啥 BitSet 不可迭代?

为啥 Javascript `iterator.next()` 返回一个对象?

为啥 ES2015 中的字符串是可迭代的?