Por que os objetos não são iteráveis ​​em JavaScript?

87

Por que os objetos não são iteráveis ​​por padrão?

Eu vejo perguntas o tempo todo relacionadas à iteração de objetos, a solução comum sendo iterar sobre as propriedades de um objeto e acessar os valores dentro de um objeto dessa maneira. Isso parece tão comum que me faz pensar por que os próprios objetos não são iteráveis.

Instruções como o ES6 for...ofseriam boas para usar para objetos por padrão. Como esses recursos estão disponíveis apenas para "objetos iteráveis" especiais que não incluem {}objetos, temos que passar por muitos obstáculos para fazer isso funcionar para os objetos para os quais queremos usá-lo.

A instrução for ... of cria um loop Iterando sobre objetos iteráveis (incluindo Array, Map, Set, objetos de argumentos e assim por diante) ...

Por exemplo, usando uma função de gerador 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);
  }
}

Os dados acima registram corretamente os dados na ordem que eu espero quando executo o código no Firefox (que suporta ES6 ):

saída de hacky para ... de

Por padrão, os {}objetos não são iteráveis, mas por quê? As desvantagens superariam os benefícios potenciais de objetos serem iteráveis? Quais são os problemas associados a isso?

Além disso, porque {}os objetos são diferentes de "array-like" coleções e "iterable objetos", como NodeList, HtmlCollection, e arguments, eles não podem ser convertidas em matrizes.

Por exemplo:

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

ou ser usado com métodos Array:

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

Além das perguntas que tenho acima, eu adoraria ver um exemplo {}prático sobre como transformar objetos em iteráveis, especialmente daqueles que mencionaram o [Symbol.iterator]. Isso deve permitir que esses novos {}"objetos iteráveis" usem instruções como for...of. Além disso, gostaria de saber se tornar os objetos iteráveis ​​permite que eles sejam convertidos em Arrays.

Tentei o código abaixo, mas recebo um 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
boombox
fonte
1
Qualquer coisa que tenha uma Symbol.iteratorpropriedade é iterável. Então, você apenas teria que implementar essa propriedade. Uma possível explicação de por que os objetos não são iteráveis ​​poderia ser que isso implicaria que tudo era iterável, já que tudo é um objeto (exceto primitivos, é claro). No entanto, o que significa iterar sobre uma função ou um objeto de expressão regular?
Felix Kling
7
Qual é a sua pergunta real aqui? Por que a ECMA tomou as decisões que tomou?
Steve Bennett
3
Uma vez que os objetos NÃO têm nenhuma ordem garantida de suas propriedades, eu me pergunto se isso rompe com a definição de um iterável que você esperaria ter uma ordem previsível?
jfriend00
2
Para obter uma resposta confiável para "por quê", você deve perguntar em esdiscuss.org
Felix Kling
1
@FelixKling - esse post é sobre ES6? Você provavelmente deve editá-lo para dizer de qual versão está falando, porque a "próxima versão do ECMAScript" não funciona muito bem com o tempo.
jfriend00

Respostas:

42

Vou tentar. Observe que não sou afiliado à ECMA e não tenho visibilidade de seu processo de tomada de decisão, portanto, não posso dizer com certeza por que eles fizeram ou não fizeram nada. No entanto, vou expor minhas suposições e dar o meu melhor.

1. Por que adicionar uma for...ofconstrução em primeiro lugar?

JavaScript já inclui uma for...inconstrução que pode ser usada para iterar as propriedades de um objeto. No entanto, não é realmente um loop forEach , já que enumera todas as propriedades em um objeto e tende a funcionar de forma previsível apenas em casos simples.

Ele quebra em casos mais complexos (incluindo com matrizes, onde seu uso tende a ser desencorajado ou completamente ofuscado pelas salvaguardas necessárias para o uso correto defor...in uma matriz ). Você pode contornar isso usando (entre outras coisas), mas isso é um pouco desajeitado e deselegante. hasOwnProperty

Portanto, minha suposição é que o for...of construto está sendo adicionado para resolver as deficiências associadas ao for...inconstruto e fornecer maior utilidade e flexibilidade ao iterar coisas. As pessoas tendem a tratar for...incomo um forEachloop que pode ser geralmente aplicado a qualquer coleção e produzir resultados sensatos em qualquer contexto possível, mas não é isso que acontece. O for...ofloop corrige isso.

Também presumo que seja importante que o código ES5 existente seja executado em ES6 e produza o mesmo resultado que em ES5, portanto, não podem ser feitas alterações significativas, por exemplo, no comportamento da for...inconstrução.

2. Como for...offunciona?

A documentação de referência é útil para esta parte. Especificamente, um objeto é considerado iterablese definir a Symbol.iteratorpropriedade.

A definição da propriedade deve ser uma função que retorna os itens na coleção, um, por, um, e define um sinalizador indicando se há ou não mais itens para buscar. Implementações predefinidas são fornecidas para alguns tipos de objeto e é relativamente claro que usar for...ofsimplesmente delegados para a função iteradora.

Essa abordagem é útil, pois torna muito simples fornecer seus próprios iteradores. Eu poderia dizer que a abordagem poderia ter apresentado problemas práticos devido à sua confiança na definição de uma propriedade onde anteriormente não havia nenhuma, exceto pelo que posso dizer que não é o caso, pois a nova propriedade é essencialmente ignorada, a menos que você deliberadamente vá procurá-la (ou seja, não se apresentará em for...inloops como uma chave, etc.). Então esse não é o caso.

Deixando de lado as questões práticas, pode ter sido considerado conceitualmente controverso iniciar todos os objetos com uma nova propriedade predefinida ou dizer implicitamente que "todo objeto é uma coleção".

3. Por que os objetos não são iterableusados for...ofpor padrão?

Meu palpite é que esta é uma combinação de:

  1. Fazendo todos os objetos iterable por padrão pode ter sido considerado inaceitável porque adiciona uma propriedade onde antes não havia nenhuma, ou porque um objeto não é (necessariamente) uma coleção. Como Felix observa, "o que significa iterar sobre uma função ou um objeto de expressão regular"?
  2. Objetos simples já podem ser iterados usando for...in, e não está claro o que uma implementação de iterador embutido poderia ter feito de forma diferente / melhor do que o for...incomportamento existente . Portanto, mesmo se o nº 1 estiver errado e adicionar a propriedade for aceitável, pode não ter sido considerado útil .
  3. Os usuários que desejam fazer seus objetos iterablepodem fazê-lo facilmente, definindo a Symbol.iteratorpropriedade.
  4. A especificação ES6 também fornece um tipo de mapa , que é iterable por padrão e tem algumas outras pequenas vantagens sobre o uso de um objeto simples como um Map.

Existe até um exemplo fornecido para o nº 3 na documentação de referência:

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

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

Dado que os objetos podem ser facilmente feitos iterable, que eles já podem ser iterados usando for...ine que provavelmente não há um acordo claro sobre o que um iterador de objeto padrão deve fazer (se o que ele faz é para ser de alguma forma diferente do que for...infaz), parece razoável o suficiente para que os objetos não fossem feitos iterablepor padrão.

Observe que seu código de exemplo pode ser reescrito usando 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"
    }
}

... ou você também pode fazer seu objeto iterableda maneira que quiser, fazendo algo como o seguinte (ou você pode fazer todos os objetos iterableatribuindo a Object.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
    };
};

Você pode brincar com isso aqui (no Chrome, pelo menos): http://jsfiddle.net/rncr3ppz/5/

Editar

E em resposta à sua pergunta atualizada, sim, é possível converter um iterableem um array, usando o operador de propagação no ES6.

No entanto, isso não parece estar funcionando no Chrome ainda, ou pelo menos não consigo fazê-lo funcionar no meu jsFiddle. Em teoria, deveria ser tão simples como:

var array = [...myIterable];
aroth
fonte
Por que não fazer obj[Symbol.iterator] = obj[Symbol.enumerate]em seu último exemplo?
Bergi
@Bergi - Porque não vi isso na documentação (e não estou vendo essa propriedade descrita aqui ). Embora um argumento a favor da definição explícita do iterador seja que ele facilita a aplicação de uma ordem de iteração específica, caso seja necessário. Se a ordem da iteração não for importante (ou se a ordem padrão for adequada) e o atalho de uma linha funcionar, há poucos motivos para não adotar a abordagem mais concisa.
aroth
Ops, [[enumerate]]não é um símbolo conhecido (@@ enumerate), mas um método interno. Eu teria que serobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi
Para que servem todas essas suposições, quando o processo real de discussão está bem documentado? É muito estranho que você diga "Portanto, minha suposição é que o construto para ... de está sendo adicionado para resolver as deficiências associadas ao construto para ... em." Não. Ele foi adicionado para oferecer suporte a uma maneira geral de iterar sobre qualquer coisa e é parte de um amplo conjunto de novos recursos, incluindo os próprios iteráveis, geradores e mapas e conjuntos. Não tem a intenção de substituir ou atualizar for...in, o que tem um propósito diferente - iterar nas propriedades de um objeto.
2
É bom enfatizar novamente que nem todo objeto é uma coleção. Objetos têm sido usados ​​como tal há muito tempo, porque era muito conveniente, mas no final das contas, eles não são realmente coleções. É isso que temos Mappor enquanto.
Felix Kling
9

Objects não implementam os protocolos de iteração em Javascript por boas razões. Existem dois níveis nos quais as propriedades do objeto podem ser iteradas em JavaScript:

  • o nível do programa
  • o nível de dados

Iteração de nível de programa

Quando você itera sobre um objeto no nível do programa, examina uma parte da estrutura do programa. É uma operação reflexiva. Vamos ilustrar essa instrução com um tipo de array, que geralmente é iterado no nível dos dados:

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

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

Acabamos de examinar o nível do programa de xs. Como os arrays armazenam sequências de dados, estamos regularmente interessados ​​apenas no nível de dados. for..inevidentemente, não faz sentido em conexão com matrizes e outras estruturas "orientadas a dados" na maioria dos casos. Esta é a razão pela qual ES2015 introduziu for..ofo protocolo iterável.

Iteração de nível de dados

Isso significa que podemos simplesmente distinguir os dados do nível do programa distinguindo funções de tipos primitivos? Não, porque as funções também podem ser dados em Javascript:

  • Array.prototype.sort por exemplo, espera que uma função execute um certo algoritmo de classificação
  • Thunks como () => 1 + 2são apenas invólucros funcionais para valores avaliados lentamente

Além disso, os valores primitivos também podem representar o nível do programa:

  • [].lengthpor exemplo, é a, Numbermas representa o comprimento de uma matriz e, portanto, pertence ao domínio do programa

Isso significa que não podemos distinguir o programa e o nível de dados apenas verificando os tipos.


É importante entender que a implementação dos protocolos de iteração para objetos JavaScript antigos simples dependeria do nível de dados. Mas, como acabamos de ver, uma distinção confiável entre dados e iteração em nível de programa não é possível.

Com Arrays, essa distinção é trivial: cada elemento com uma chave semelhante a um inteiro é um elemento de dados. Objects têm um recurso equivalente: o enumerabledescritor. Mas é realmente aconselhável confiar nisso? Eu acredito que não é! O significado do enumerabledescritor é muito borrado.

Conclusão

Não há uma maneira significativa de implementar os protocolos de iteração para objetos, porque nem todo objeto é uma coleção.

Se as propriedades do objeto fossem iteráveis ​​por padrão, o programa e o nível de dados eram misturados. Uma vez que cada tipo composto em Javascript é baseado em objetos simples, isso se aplicaria para Arraye Mapbem.

for..in, Object.keys , Reflect.ownKeysEtc, podem ser usados tanto para a reflexão e dados iteração, uma clara distinção não é regularmente possível. Se você não for cuidadoso, acabará rapidamente com metaprogramação e dependências estranhas. O Maptipo de dados abstratos efetivamente termina a fusão do nível de programa e dados. Acredito que Mapseja a conquista mais significativa no ES2015, embora Promiseseja muito mais emocionante.


fonte
3
+1, eu penso "Não há maneira significativa de implementar os protocolos de iteração para objetos, porque nem todo objeto é uma coleção." resume tudo.
Charlie Schliesser
1
Não acho que seja um bom argumento. Se o seu objeto não é uma coleção, por que você está tentando fazer um loop sobre ele? Não importa que nem todo objeto seja uma coleção, porque você não tentará iterar sobre os que não são.
BT
Na verdade, todo objeto é uma coleção, e não cabe à linguagem decidir se a coleção é coerente ou não. Matrizes e mapas também podem coletar valores não relacionados. O ponto é que você pode iterar as chaves de qualquer objeto, independentemente de seu uso, portanto, você está a um passo de iterar seus valores. Se você estivesse falando sobre uma linguagem que digita estaticamente valores de array (ou qualquer outra coleção), você poderia falar sobre essas restrições, mas não sobre JavaScript.
Manngo
Esse argumento de que todo objeto não é uma coleção não faz sentido. Você está assumindo que um iterador tem apenas um propósito (iterar uma coleção). O iterador padrão em um objeto seria um iterador das propriedades do objeto, não importa o que essas propriedades representem (seja uma coleção ou outra coisa). Como disse o Manngo, se o seu objeto não representa uma coleção, cabe ao programador não tratá-lo como uma coleção. Talvez eles apenas queiram iterar as propriedades no objeto para alguma saída de depuração? Existem muitos outros motivos além de uma coleção.
jfriend00
8

Acho que a pergunta deveria ser "por que não há iteração de objeto embutido ?

Adicionar iterabilidade aos próprios objetos pode ter consequências indesejadas e não, não há como garantir a ordem, mas escrever um iterador é tão simples quanto

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

Então

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

a 1
b 2

fonte
1
obrigado torazaburo. eu revisei minha pergunta. Eu adoraria ver um exemplo usando [Symbol.iterator], bem como se você pudesse expandir essas consequências não intencionais.
boombox de
4

Você pode facilmente tornar todos os objetos iteráveis ​​globalmente:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});
Jack Slocum
fonte
3
Não adicione métodos globalmente a objetos nativos. Essa é uma ideia terrível que vai morder a sua bunda, e a qualquer um que use seu código.
BT
2

Esta é a abordagem mais recente (que funciona em canário cromado)

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

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

Sim, entriesagora é um método que faz parte do Object :)

editar

Depois de analisar mais a fundo, parece que você poderia fazer o seguinte

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

então agora você pode fazer isso

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

edit2

Voltando ao seu exemplo original, se você quiser usar o método de protótipo acima, gostaria que este

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);
    }
}
Chad Scira
fonte
2

Eu também estava incomodado com esta questão.

Então tive a ideia de usar Object.entries({...}), ele retorna um Arrayque é um Iterable.

Além disso, o Dr. Axel Rauschmayer postou uma excelente resposta sobre isso. Veja por que objetos simples NÃO são iteráveis

Romko
fonte
0

Tecnicamente, esta não é uma resposta à pergunta por quê? mas adaptei a resposta de Jack Slocum acima à luz dos comentários de BT para algo que pode ser usado para tornar um objeto iterável.

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

Não é tão conveniente quanto deveria, mas é viável, especialmente se você tiver vários objetos:

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

E, como cada um tem direito a uma opinião, também não consigo ver por que os objetos já não são iteráveis. Se você pode polyfill como acima, ou usefor … in então não consigo ver um argumento simples.

Uma sugestão possível é que o que é iterável é um tipo de objeto, então é possível que iterável tenha sido limitado a um subconjunto de objetos, apenas no caso de algum outro objeto explodir na tentativa.

Manngo
fonte