Trasformazione di un iteratore Javascript in un array


171

Sto cercando di utilizzare il nuovo oggetto Map da Javascript EC6, poiché è già supportato nelle ultime versioni di Firefox e Chrome.

Ma lo trovo molto limitato nella programmazione "funzionale", perché manca di metodi di mappatura, filtro ecc. Classici che funzionerebbero bene con una [key, value]coppia. Ha un forEach ma questo NON restituisce il risultato della richiamata.

Se potessi trasformarlo map.entries()da un MapIterator in un semplice array, allora potrei usare lo standard .map, .filtersenza ulteriori hack.

Esiste un "buon" modo per trasformare un iteratore Javascript in un array? In Python è facile come fare list(iterator)... ma Array(m.entries())restituisci un array con Iterator come primo elemento !!!

MODIFICARE

Ho dimenticato di specificare che sto cercando una risposta che funzioni ovunque Map funzioni, il che significa almeno Chrome e Firefox (Array.from non funziona in Chrome).

PS.

So che c'è il fantastico wu.js ma la sua dipendenza da traceur mi mette fuori di testa ...


Risposte:


247

Stai cercando la nuova Array.fromfunzione che converte iterabili arbitrari in istanze di array:

var arr = Array.from(map.entries());

Ora è supportato in Edge, FF, Chrome e Node 4+ .

Naturalmente, potrebbe essere utile per definire map, filtere metodi simili direttamente sull'interfaccia iteratore, così che si può evitare di allocare la matrice. È inoltre possibile utilizzare una funzione del generatore anziché le funzioni di ordine superiore:

function* map(iterable) {
    var i = 0;
    for (var item of iterable)
        yield yourTransformation(item, i++);
}
function* filter(iterable) {
    var i = 0;
    for (var item of iterable)
        if (yourPredicate(item, i++))
             yield item;
}

Mi aspetterei che il callback riceva (value, key)coppie e non (value, index)coppie.
Aadit M Shah,

3
@AaditMShah: qual è la chiave di un iteratore? Naturalmente, se ripetessi una mappa, potresti definireyourTransformation = function([key, value], index) { … }
Bergi,

Un iteratore non ha una chiave ma a Mapha coppie di valori chiave. Quindi, secondo la mia modesta opinione, non ha senso definire il generale mape le filterfunzioni per gli iteratori. Invece, ogni oggetto iterabile dovrebbe avere il proprio mape filterfunzioni. Questo ha senso perché mape filtersono operazioni che preservano la struttura (forse no, filterma mapcertamente lo sono) e quindi le funzioni mape filterdovrebbero conoscere la struttura degli oggetti iterabili su cui stanno mappando o filtrando. Pensaci, in Haskell definiamo diverse istanze di Functor. =)
Aadit M Shah,

1
@Stefano: Si può shim è facilmente ...
Bergi

1
@Incognito Ah ok, certo che è vero, ma è proprio quello che la domanda sta ponendo, non un problema con la mia risposta.
Bergi,

45

[...map.entries()] o Array.from(map.entries())

È semplicissimo.

Comunque - gli iteratori non hanno metodi di riduzione, filtro e simili. Devi scriverli da soli, in quanto è più efficace della conversione di Map in array e viceversa. Ma non fare salti Mappa -> Matrice -> Mappa -> Matrice -> Mappa -> Matrice, perché ucciderà le prestazioni.


1
A meno che tu non abbia qualcosa di più sostanziale, questo dovrebbe davvero essere un commento. Inoltre, Array.fromè già stato coperto da @Bergi.
Aadit M Shah,

2
E, come ho scritto nella mia domanda originale, [iterator]non funziona perché in Chrome crea un array con un singolo iteratorelemento al suo interno e [...map.entries()]non è una sintassi accettata in Chrome
Stefano,

2
L' operatore di diffusione @Stefano è ora accettato la sintassi in Chrome
Klesun,

15

Non è necessario trasformare a Mapin Array. Potresti semplicemente creare mape filterfunzioni per gli Mapoggetti:

function map(functor, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        result.set(key, functor.call(this, value, key, object));
    }, self);

    return result;
}

function filter(predicate, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        if (predicate.call(this, value, key, object)) result.set(key, value);
    }, self);

    return result;
}

Ad esempio, potresti aggiungere un botto (ovvero un !carattere) al valore di ciascuna voce di una mappa la cui chiave è una primitiva.

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = map(appendBang, filter(primitive, object));

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
function map(functor, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        result.set(key, functor.call(this, value, key, object));
    }, self || null);

    return result;
}

function filter(predicate, object, self) {
    var result = new Map;

    object.forEach(function (value, key, object) {
        if (predicate.call(this, value, key, object)) result.set(key, value);
    }, self || null);

    return result;
}
</script>

È inoltre possibile aggiungere mape filtermetodi Map.prototypeper farlo leggere meglio. Sebbene in genere non sia consigliabile modificare i prototipi nativi, tuttavia credo che un'eccezione possa essere fatta nel caso di mape filterper Map.prototype:

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = object.filter(primitive).map(appendBang);

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
Map.prototype.map = function (functor, self) {
    var result = new Map;

    this.forEach(function (value, key, object) {
        result.set(key, functor.call(this, value, key, object));
    }, self || null);

    return result;
};

Map.prototype.filter = function (predicate, self) {
    var result = new Map;

    this.forEach(function (value, key, object) {
        if (predicate.call(this, value, key, object)) result.set(key, value);
    }, self || null);

    return result;
};
</script>


Modifica: nella risposta di Bergi, ha creato funzioni generiche mape filtergeneratrici per tutti gli oggetti iterabili. Il vantaggio di usarli è che, poiché sono funzioni di generatore, non allocano oggetti iterabili intermedi.

Ad esempio, my mape filterfunzioni definite sopra creano nuovi Mapoggetti. Quindi la chiamata object.filter(primitive).map(appendBang)crea due nuovi Mapoggetti:

var intermediate = object.filter(primitive);
var result = intermediate.map(appendBang);

La creazione di oggetti iterabili intermedi è costosa. Le funzioni del generatore di Bergi risolvono questo problema. Non allocano oggetti intermedi ma consentono a un iteratore di alimentare pigramente i suoi valori a quello successivo. Questo tipo di ottimizzazione è nota come fusione o deforestazione nei linguaggi di programmazione funzionale e può migliorare significativamente le prestazioni del programma.

L'unico problema che ho con le funzioni del generatore di Bergi è che non sono specifiche degli Mapoggetti. Invece, sono generalizzati per tutti gli oggetti iterabili. Quindi invece di chiamare le funzioni di callback con le (value, key)coppie (come mi aspetterei quando si mappasse su a Map), chiama le funzioni di callback con le (value, index)coppie. Altrimenti, è un'ottima soluzione e consiglierei sicuramente di usarla rispetto alle soluzioni fornite.

Quindi queste sono le funzioni specifiche del generatore che vorrei utilizzare per mappare e filtrare gli Mapoggetti:

function * map(functor, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        yield [key, functor.call(that, value, key, entries)];
    }
}

function * filter(predicate, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key    = entry[0];
        var value  = entry[1];

        if (predicate.call(that, value, key, entries)) yield [key, value];
    }
}

function toMap(entries) {
    var result = new Map;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        result.set(key, value);
    }

    return result;
}

function toArray(entries) {
    var array = [];

    for (var entry of entries) {
        array.push(entry[1]);
    }

    return array;
}

Possono essere usati come segue:

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = toMap(map(appendBang, filter(primitive, object.entries())));

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

var array  = toArray(map(appendBang, filter(primitive, object.entries())));

alert(JSON.stringify(array, null, 4));

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
function * map(functor, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        yield [key, functor.call(that, value, key, entries)];
    }
}

function * filter(predicate, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key    = entry[0];
        var value  = entry[1];

        if (predicate.call(that, value, key, entries)) yield [key, value];
    }
}

function toMap(entries) {
    var result = new Map;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        result.set(key, value);
    }

    return result;
}

function toArray(entries) {
    var array = [];

    for (var entry of entries) {
        array.push(entry[1]);
    }

    return array;
}
</script>

Se desideri un'interfaccia più fluida, potresti fare qualcosa del genere:

var object = new Map;

object.set("", "empty string");
object.set(0,  "number zero");
object.set(object, "itself");

var result = new MapEntries(object).filter(primitive).map(appendBang).toMap();

alert(result.get(""));     // empty string!
alert(result.get(0));      // number zero!
alert(result.get(object)); // undefined

var array  = new MapEntries(object).filter(primitive).map(appendBang).toArray();

alert(JSON.stringify(array, null, 4));

function primitive(value, key) {
    return isPrimitive(key);
}

function appendBang(value) {
    return value + "!";
}

function isPrimitive(value) {
    var type = typeof value;
    return value === null ||
        type !== "object" &&
        type !== "function";
}
<script>
MapEntries.prototype = {
    constructor: MapEntries,
    map: function (functor, self) {
        return new MapEntries(map(functor, this.entries, self), true);
    },
    filter: function (predicate, self) {
        return new MapEntries(filter(predicate, this.entries, self), true);
    },
    toMap: function () {
        return toMap(this.entries);
    },
    toArray: function () {
        return toArray(this.entries);
    }
};

function MapEntries(map, entries) {
    this.entries = entries ? map : map.entries();
}

function * map(functor, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        yield [key, functor.call(that, value, key, entries)];
    }
}

function * filter(predicate, entries, self) {
    var that = self || null;

    for (var entry of entries) {
        var key    = entry[0];
        var value  = entry[1];

        if (predicate.call(that, value, key, entries)) yield [key, value];
    }
}

function toMap(entries) {
    var result = new Map;

    for (var entry of entries) {
        var key   = entry[0];
        var value = entry[1];

        result.set(key, value);
    }

    return result;
}

function toArray(entries) {
    var array = [];

    for (var entry of entries) {
        array.push(entry[1]);
    }

    return array;
}
</script>

Spero che aiuti.


lo fa grazie! Dare il voto positivo a @Bergi perché non conoscevo "Array.from" e questo è il punto più preciso. Discussione molto interessante anche tra te!
Stefano,

1
@Stefano Ho modificato la mia risposta per mostrare come i generatori possono essere utilizzati per trasformare correttamente gli Mapoggetti utilizzando funzioni specializzate mape filter. La risposta di Bergi dimostra l'uso di generici mape filterfunzioni per tutti gli oggetti iterabili che non possono essere usati per trasformare Mapoggetti perché le chiavi Mapdell'oggetto sono perse.
Aadit M Shah,

Wow, mi piace molto la tua modifica. Ho finito per scrivere la mia risposta qui: stackoverflow.com/a/28721418/422670 (aggiunto lì poiché questa domanda è stata chiusa come duplicata) perché Array.fromnon funziona in Chrome (mentre Map e iteratori lo fanno!). Ma vedo che l'approccio è molto simile e potresti semplicemente aggiungere la funzione "toArray" al tuo gruppo!
Stefano,

1
@Stefano Infatti. Ho modificato la mia risposta per mostrare come aggiungere una toArrayfunzione.
Aadit M Shah,

7

Un piccolo aggiornamento dal 2019:

Ora Array.from sembra essere universalmente disponibile e, inoltre, accetta un secondo argomento mapFn , che gli impedisce di creare un array intermedio. Questo in pratica sembra così:

Array.from(myMap.entries(), entry => {...});

poiché Array.fromesiste già una risposta , questo è più adatto per essere un commento o una modifica richiesta per quella risposta ... ma grazie!
Stefano

1

È possibile utilizzare una libreria come https://www.npmjs.com/package/itiriri che implementa metodi simili a array per gli iterabili:

import { query } from 'itiriri';

const map = new Map();
map.set(1, 'Alice');
map.set(2, 'Bob');

const result = query(map)
  .filter([k, v] => v.indexOf('A') >= 0)
  .map([k, v] => `k - ${v.toUpperCase()}`);

for (const r of result) {
  console.log(r); // prints: 1 - ALICE
}

Questa lib sembra incredibile e il condotto mancante per passare a iterables @dimadeveatii - grazie mille per averlo scritto, lo proverò presto :-)
Angelos Pikoulas,

0

È possibile ottenere l' array di array (chiave e valore):

[...this.state.selected.entries()]
/**
*(2) [Array(2), Array(2)]
*0: (2) [2, true]
*1: (2) [3, true]
*length: 2
*/

Quindi, puoi facilmente ottenere valori dall'interno, come ad esempio le chiavi con l'iteratore della mappa.

[...this.state.selected[asd].entries()].map(e=>e[0])
//(2) [2, 3]

0

Puoi anche usare fluente-iterabile per trasformare in array:

const iterable: Iterable<T> = ...;
const arr: T[] = fluent(iterable).toArray();
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.