Perché gli oggetti non sono iterabili in JavaScript?


87

Perché gli oggetti non sono iterabili per impostazione predefinita?

Vedo continuamente domande relative all'iterazione di oggetti, la soluzione comune è scorrere le proprietà di un oggetto e accedere ai valori all'interno di un oggetto in questo modo. Questo sembra così comune che mi chiedo perché gli oggetti stessi non siano iterabili.

Dichiarazioni come ES6 for...ofsarebbero piacevoli da usare per gli oggetti per impostazione predefinita. Poiché queste funzionalità sono disponibili solo per speciali "oggetti iterabili" che non includono {}oggetti, dobbiamo passare attraverso i cerchi per farlo funzionare per gli oggetti per i quali vogliamo usarlo.

L'istruzione for ... of crea un ciclo che scorre su oggetti iterabili (inclusi Array, Map, Set, arguments object e così via) ...

Ad esempio utilizzando una funzione del generatore 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);
  }
}

Quanto sopra registra correttamente i dati nell'ordine in cui mi aspetto quando eseguo il codice in Firefox (che supporta ES6 ):

output di hacky per ... di

Per impostazione predefinita, gli {}oggetti non sono iterabili, ma perché? Gli svantaggi supererebbero i potenziali vantaggi di oggetti iterabili? Quali sono i problemi associati a questo?

Inoltre, poiché {}gli oggetti sono diversi da "Array-like" collezioni e "iterable oggetti" come NodeList, HtmlCollectione arguments, non possono essere convertiti in array.

Per esempio:

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

o essere utilizzato con i metodi Array:

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

Oltre alle domande che ho sopra, mi piacerebbe vedere un esempio funzionante su come trasformare gli {}oggetti in iterabili, specialmente da coloro che hanno menzionato il file [Symbol.iterator]. Ciò dovrebbe consentire a questi nuovi {}"oggetti iterabili" di utilizzare istruzioni come for...of. Inoltre, mi chiedo se rendere gli oggetti iterabili consenta loro di essere convertiti in array.

Ho provato il codice seguente, ma ottengo un file 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
Tutto ciò che ha una Symbol.iteratorproprietà è un iterabile. Quindi dovresti solo implementare quella proprietà. Una possibile spiegazione del motivo per cui gli oggetti non sono iterabili potrebbe essere che questo implicherebbe che tutto fosse iterabile, poiché tutto è un oggetto (tranne le primitive ovviamente). Tuttavia, cosa significa iterare su una funzione o un oggetto di espressione regolare?
Felix Kling

7
Qual è la tua vera domanda qui? Perché l'ECMA ha preso le decisioni che ha preso?
Steve Bennett

3
Poiché gli oggetti NON hanno un ordine garantito delle loro proprietà, mi chiedo se ciò rompe con la definizione di un iterabile che ti aspetteresti di avere un ordine prevedibile?
jfriend00

2
Per ottenere una risposta autorevole al "perché", dovresti chiedere a esdiscuss.org
Felix Kling,

1
@FelixKling - quel post riguarda ES6? Probabilmente dovresti modificarlo per dire di quale versione stai parlando perché la "prossima versione di ECMAScript" non funziona molto bene nel tempo.
jfriend00

Risposte:


42

Ci proverò. Nota che non sono affiliato con ECMA e non ho visibilità sul loro processo decisionale, quindi non posso dire con certezza perché hanno o non hanno fatto nulla. Tuttavia, esporrò le mie ipotesi e farò del mio meglio.

1. Perché aggiungere un for...ofcostrutto in primo luogo?

JavaScript include già un for...incostrutto che può essere utilizzato per iterare le proprietà di un oggetto. Tuttavia, non è realmente un ciclo forEach , poiché enumera tutte le proprietà su un oggetto e tende a funzionare in modo prevedibile solo in casi semplici.

Si rompe in casi più complessi (incluso con gli array, dove il suo utilizzo tende ad essere scoraggiato o completamente offuscato dalle protezioni necessarie per un uso correttofor...in con un array ). Puoi aggirare questo problema usando (tra le altre cose), ma è un po 'goffo e inelegante. hasOwnProperty

Pertanto, la mia ipotesi è che il for...ofcostrutto venga aggiunto per affrontare le carenze associate al for...incostrutto e fornire maggiore utilità e flessibilità durante l'iterazione delle cose. Le persone tendono a trattare for...income un forEachciclo che può essere generalmente applicato a qualsiasi raccolta e produce risultati sani in ogni possibile contesto, ma non è quello che succede. Il for...ofciclo lo risolve.

Presumo anche che sia importante che il codice ES5 esistente venga eseguito sotto ES6 e produca lo stesso risultato di ES5, quindi non è possibile apportare modifiche di rilievo, ad esempio, al comportamento del for...incostrutto.

2. Come for...offunziona?

La documentazione di riferimento è utile per questa parte. Nello specifico, un oggetto viene considerato iterablese definisce la Symbol.iteratorproprietà.

La definizione della proprietà dovrebbe essere una funzione che restituisce gli elementi nella raccolta, uno, per, uno e imposta un flag che indica se ci sono o meno più elementi da recuperare. Vengono fornite implementazioni predefinite per alcuni tipi di oggetto ed è relativamente chiaro che l'utilizzo di for...ofdelegati semplicemente alla funzione iteratore.

Questo approccio è utile, poiché rende molto semplice fornire i propri iteratori. Potrei dire che l'approccio avrebbe potuto presentare problemi pratici a causa della sua dipendenza dalla definizione di una proprietà dove in precedenza non ce n'era, tranne da quello che posso dire che non è così in quanto la nuova proprietà viene essenzialmente ignorata a meno che tu non la cerchi deliberatamente non sarà presente nei for...inloop come chiave, ecc.). Quindi non è così.

Pratiche non-questioni a parte, potrebbe essere stato considerato concettualmente controverso iniziare tutti gli oggetti con una nuova proprietà predefinita, o affermare implicitamente che "ogni oggetto è una collezione".

3. Perché gli oggetti non vengono iterableutilizzati for...ofper impostazione predefinita?

La mia ipotesi è che questa sia una combinazione di:

  1. Rendere tutti gli oggetti iterableper impostazione predefinita potrebbe essere stato considerato inaccettabile perché aggiunge una proprietà dove prima non ce n'era, o perché un oggetto non è (necessariamente) una collezione. Come nota Felix, "cosa significa iterare su una funzione o un oggetto di espressione regolare"?
  2. Oggetti semplici possono già essere iterati usando for...in, e non è chiaro cosa avrebbe potuto fare un'implementazione iteratore integrato in modo diverso / migliore rispetto al for...incomportamento esistente . Quindi, anche se il numero 1 è sbagliato e l'aggiunta della proprietà era accettabile, potrebbe non essere stata considerata utile .
  3. Gli utenti che desiderano creare i propri oggetti iterablepossono farlo facilmente definendo la Symbol.iteratorproprietà.
  4. La specifica ES6 fornisce anche un tipo di mappa , che è iterable di default e presenta alcuni altri piccoli vantaggi rispetto all'uso di un oggetto semplice come file Map.

C'è anche un esempio fornito per # 3 nella documentazione di riferimento:

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

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

Dato che gli oggetti possono essere facilmente creati iterable, che possono già essere iterati usando for...ine che probabilmente non c'è un chiaro accordo su cosa dovrebbe fare un iteratore di oggetti predefinito (se ciò che fa deve essere in qualche modo diverso da ciò che for...infa), sembra ragionevole abbastanza che gli oggetti non fossero creati iterabledi default.

Tieni presente che il codice di esempio può essere riscritto utilizzando 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"
    }
}

... oppure puoi anche creare il tuo oggetto iterablenel modo che preferisci facendo qualcosa di simile al seguente (oppure puoi creare tutti gli oggetti iterableassegnando Object.prototype[Symbol.iterator]invece a):

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

Puoi giocarci qui (in Chrome, a noleggio): http://jsfiddle.net/rncr3ppz/5/

modificare

E in risposta alla tua domanda aggiornata, sì, è possibile convertire un iterablein un array, utilizzando l' operatore spread in ES6.

Tuttavia, questo non sembra ancora funzionare in Chrome, o almeno non riesco a farlo funzionare nel mio jsFiddle. In teoria dovrebbe essere semplice come:

var array = [...myIterable];

Perché non farlo solo obj[Symbol.iterator] = obj[Symbol.enumerate]nel tuo ultimo esempio?
Bergi

@Bergi - Perché non l'ho visto nei documenti (e non vedo quella proprietà descritta qui ). Sebbene un argomento a favore della definizione esplicita dell'iteratore sia che semplifica l'applicazione di uno specifico ordine di iterazione, se necessario. Se l'ordine di iterazione non è importante (o se l'ordine predefinito va bene) e la scorciatoia di una riga funziona, ci sono poche ragioni per non adottare l'approccio più conciso, tuttavia.
aroth

Oops, [[enumerate]]non è un simbolo noto (@@ enumerate) ma un metodo interno. Dovrei essereobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi

A cosa servono tutte queste supposizioni, quando l'effettivo processo di discussione è ben documentato? È molto strano che tu dica "Quindi, quindi, la mia ipotesi è che il costrutto for ... of venga aggiunto per affrontare le carenze associate al costrutto for ... in." No. È stato aggiunto per supportare un modo generale di iterare su qualsiasi cosa e fa parte di un ampio set di nuove funzionalità, inclusi gli iterabili stessi, i generatori, le mappe e i set. Non è certo inteso come una sostituzione o un aggiornamento a for...in, che ha uno scopo diverso: iterare attraverso le proprietà di un oggetto.

2
Buon punto sottolineando ancora una volta che non tutti gli oggetti sono una collezione. Gli oggetti sono stati usati come tali per molto tempo, perché era molto conveniente, ma alla fine non sono propriamente collezioni. Questo è quello che abbiamo Mapper ora.
Felix Kling

9

Objects non implementano i protocolli di iterazione in Javascript per ottime ragioni. Ci sono due livelli in cui le proprietà degli oggetti possono essere ripetute in JavaScript:

  • il livello del programma
  • il livello dei dati

Iterazione a livello di programma

Quando si itera su un oggetto a livello di programma, si esamina una parte della struttura del programma. È un'operazione riflessiva. Illustriamo questa affermazione con un tipo di array, che di solito viene ripetuto a livello di dati:

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

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

Abbiamo appena esaminato il livello del programma di xs. Poiché gli array memorizzano sequenze di dati, siamo regolarmente interessati solo al livello di dati. for..inevidentemente non ha senso in relazione ad array e altre strutture "orientate ai dati" nella maggior parte dei casi. Questo è il motivo per cui ES2015 ha introdotto for..ofe il protocollo iterabile.

Iterazione a livello di dati

Ciò significa che possiamo semplicemente distinguere i dati dal livello di programma distinguendo le funzioni dai tipi primitivi? No, perché le funzioni possono anche essere dati in Javascript:

  • Array.prototype.sort per esempio si aspetta che una funzione esegua un certo algoritmo di ordinamento
  • I thunk come () => 1 + 2sono solo wrapper funzionali per valori valutati pigramente

Oltre ai valori primitivi possono rappresentare anche il livello del programma:

  • [].lengthper esempio è un Numberma rappresenta la lunghezza di un array e quindi appartiene al dominio del programma

Ciò significa che non possiamo distinguere il programma e il livello dei dati semplicemente controllando i tipi.


È importante capire che l'implementazione dei protocolli di iterazione per semplici vecchi oggetti Javascript si baserebbe sul livello dei dati. Ma come abbiamo appena visto, una distinzione affidabile tra dati e iterazione a livello di programma non è possibile.

Con Arrays questa distinzione è banale: ogni elemento con una chiave simile a un intero è un elemento di dati. Objects hanno una caratteristica equivalente: il enumerabledescrittore. Ma è davvero consigliabile affidarsi a questo? Credo di no! Il significato dienumerable descrittore è troppo sfocato.

Conclusione

Non esiste un modo significativo per implementare i protocolli di iterazione per gli oggetti, perché non tutti gli oggetti sono una raccolta.

Se le proprietà degli oggetti erano iterabili per impostazione predefinita, il programma e il livello dei dati erano confusi. Poiché ogni tipo composito in Javascript è basato su oggetti semplici, ciò si applicherebbe anche a Arraye Map.

for..in, Object.keys, Reflect.ownKeysEcc può essere utilizzato sia per riflessione e dati iterazione, una chiara distinzione non è regolarmente possibile. Se non stai attento, ti ritroverai rapidamente con la meta programmazione e le strane dipendenze. Il Maptipo di dati astratto termina efficacemente la fusione del livello di programma e di dati. Credo che Mapsia il risultato più significativo in ES2015, anche se Promisesono molto più eccitanti.


3
+1, penso "Non esiste un modo significativo per implementare i protocolli di iterazione per gli oggetti, perché non tutti gli oggetti sono una raccolta." lo riassume.
Charlie Schliesser

1
Non credo sia un buon argomento. Se il tuo oggetto non è una collezione, perché stai cercando di copiarlo? Non importa che non tutti gli oggetti siano una raccolta, perché non tenterai di iterare su quelli che non lo sono.
BT

In realtà, ogni oggetto è una collezione e non spetta al linguaggio decidere se la collezione è coerente o meno. Gli array e le mappe possono anche raccogliere valori non correlati. Il punto è che puoi iterare sulle chiavi di qualsiasi oggetto indipendentemente dal loro utilizzo, quindi sei a un passo dall'iterare sui loro valori. Se stavi parlando di un linguaggio che digita staticamente i valori di array (o qualsiasi altra raccolta), potresti parlare di tali restrizioni, ma non di JavaScript.
Manngo

L'argomento secondo cui ogni oggetto non è una collezione non ha senso. Stai assumendo che un iteratore abbia un solo scopo (iterare una raccolta). L'iteratore predefinito su un oggetto sarebbe un iteratore delle proprietà dell'oggetto, indipendentemente da ciò che queste proprietà rappresentano (se una raccolta o qualcos'altro). Come ha detto Manngo, se il tuo oggetto non rappresenta una collezione, spetta al programmatore non trattarlo come una collezione. Forse vogliono solo iterare le proprietà sull'oggetto per qualche output di debug? Ci sono molte altre ragioni oltre a una collezione.
jfriend00

8

Immagino che la domanda dovrebbe essere "perché non c'è iterazione di oggetti incorporata ?

L'aggiunta di iterabilità agli oggetti stessi potrebbe plausibilmente avere conseguenze non intenzionali, e no, non c'è modo di garantire l'ordine, ma scrivere un iteratore è semplice come

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

Poi

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

a 1
b 2

1
grazie torazaburo. ho rivisto la mia domanda. mi piacerebbe vedere un esempio che utilizza [Symbol.iterator]così come se potessi espandere su quelle conseguenze non intenzionali.
boombox

4

Puoi facilmente rendere iterabili tutti gli oggetti a livello globale:

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

3
Non aggiungere metodi a livello globale agli oggetti nativi. Questa è un'idea terribile che morderà te, e chiunque utilizzi il tuo codice, nel culo.
BT

2

Questo è l'ultimo approccio (che funziona in chrome canary)

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

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

entries ora è un metodo che fa parte di Object :)

modificare

Dopo aver esaminato di più, sembra che potresti fare quanto segue

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

quindi ora puoi farlo

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

modifica 2

Tornando al tuo esempio originale, se volessi usare il metodo del prototipo di cui sopra, sarebbe come questo

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

Tecnicamente, questa non è una risposta alla domanda: perché? ma ho adattato la risposta di Jack Slocum sopra alla luce dei commenti di BT a qualcosa che può essere utilizzato per rendere iterabile un oggetto.

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

Non è così conveniente come avrebbe dovuto essere, ma è praticabile, soprattutto se hai più oggetti:

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

E, poiché ognuno ha diritto a un'opinione, non riesco a capire perché nemmeno gli oggetti siano già iterabili. Se puoi polyfill come sopra, o usafor … in non riesco a vedere un semplice argomento.

Un possibile suggerimento è che ciò che è iterabile sia un file tipo di oggetto, quindi è possibile che iterabile sia stato limitato a un sottoinsieme di oggetti nel caso in cui altri oggetti esplodano nel tentativo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.