Przekształcenie iteratora JavaScript w tablicę

171

Próbuję użyć nowego obiektu Map z Javascript EC6, ponieważ jest już obsługiwany w najnowszych wersjach przeglądarki Firefox i Chrome.

Ale uważam, że jest to bardzo ograniczone w programowaniu "funkcjonalnym", ponieważ brakuje w nim klasycznych metod mapowania, filtrów itp., Które dobrze działałyby z [key, value] parą. Ma forEach, ale to NIE zwraca wyniku wywołania zwrotnego.

Gdybym mógł przekształcić go map.entries()z MapIteratora w prostą tablicę, mógłbym użyć standardu.map , .filterbez dodatkowych hacków.

Czy istnieje „dobry” sposób na przekształcenie Iteratora JavaScript w tablicę? W Pythonie jest to tak proste, jak zrobienie list(iterator)... ale Array(m.entries())zwróć tablicę z Iteratorem jako pierwszym elementem !!!

EDYTOWAĆ

Zapomniałem sprecyzować, że szukam odpowiedzi, która działa wszędzie tam, gdzie działa Mapa, czyli przynajmniej Chrome i Firefox (Array.from nie działa w Chrome).

PS.

Wiem, że istnieje fantastyczny wu.js, ale jego zależność od traceur mnie zniechęca ...

Stefano
źródło
Zobacz także stackoverflow.com/q/27612713/1460043
user1460043

Odpowiedzi:

247

Szukasz nowej Array.fromfunkcji, która konwertuje dowolne iterable na instancje tablic:

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

Jest teraz obsługiwany w Edge, FF, Chrome i Node 4+ .

Oczywiście, być może warto zdefiniować map, filteri podobne metody bezpośrednio na interfejsie iteracyjnej, dzięki czemu można uniknąć alokacji tablicy. Możesz także użyć funkcji generatora zamiast funkcji wyższego rzędu:

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;
}
Bergi
źródło
Spodziewałbym się, że wywołanie zwrotne otrzyma (value, key)pary, a nie (value, index)pary.
Aadit M Shah
3
@AaditMShah: Co to jest klucz iteratora? Oczywiście, jeśli chcesz iterować mapę, możesz zdefiniowaćyourTransformation = function([key, value], index) { … }
Bergi,
Iterator nie ma klucza, ale Mapma pary klucz-wartość. Stąd, moim skromnym zdaniem, nie ma sensu definiowanie generała mapi filterfunkcji dla iteratorów. Zamiast tego każdy iterowalny obiekt powinien mieć swoje własne funkcje mapi filterfunkcje. Ma to sens, ponieważ operacje zachowujące strukturę mapi filtersą operacjami zachowującymi strukturę (być może nie, filterale na mappewno tak jest), a zatem funkcje mapi filterpowinny znać strukturę iterowalnych obiektów, nad którymi mapują lub filtrują. Pomyśl o tym, w Haskell definiujemy różne przypadki Functor. =)
Aadit M Shah
1
@Stefano: Z łatwością możesz to zmienić …
Bergi,
1
@Incognito Ach ok, jasne, że to prawda, ale o to właśnie chodzi w pytaniu, a nie o problem z moją odpowiedzią.
Bergi
45

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

To bardzo łatwe.

W każdym razie - w iteratorach brakuje metody redukującej, filtrującej i podobnych. Musisz napisać je samodzielnie, ponieważ jest to bardziej wydajne niż konwersja Map na tablicę iz powrotem. Ale nie rób skoków Mapa -> Array -> Map -> Array -> Map -> Array, ponieważ to zabije wydajność.

Ginden
źródło
1
Jeśli nie masz czegoś bardziej konkretnego, to naprawdę powinien być komentarz. Ponadto Array.fromzostał już objęty @Bergi.
Aadit M Shah
2
I, jak napisałem w moim pierwotnym pytaniu, [iterator]nie działa, ponieważ w Chrome tworzy tablicę z jednym iteratorelementem, a [...map.entries()]nie jest akceptowaną składnią w Chrome
Stefano
2
Operator rozprzestrzeniania @Stefano jest teraz akceptowany w Chrome
Klesun
15

Nie ma potrzeby przekształcania pliku Mapw Array. Możesz po prostu utworzyć mapi filterfunkcjonować dla Mapobiektów:

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

Na przykład, możesz dołączyć huk (tj. !Znak) do wartości każdego wpisu mapy, której klucz jest prymitywem.

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>

Możesz także dodać mapi filtermetody, Map.prototypeaby lepiej czytać. Chociaż generalnie nie zaleca się modyfikowania natywnych prototypów, uważam, że można zrobić wyjątek w przypadku mapi filterdla 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>


Edycja: w odpowiedzi Bergi stworzył ogólne mapifilter funkcje generujące dla wszystkich iterowalnych obiektów. Zaletą ich używania jest to, że ponieważ są to funkcje generujące, nie przydzielają pośrednich iterowalnych obiektów.

Na przykład funkcje my mapi filterzdefiniowane powyżej tworzą nowe Mapobiekty. Stąd wywołanie object.filter(primitive).map(appendBang)tworzy dwa nowe Mapobiekty:

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

Tworzenie pośrednich obiektów iterowalnych jest kosztowne. Funkcje generatora Bergiego rozwiązują ten problem. Nie alokują obiektów pośrednich, ale pozwalają jednemu iteratorowi na leniwe przekazywanie swoich wartości do następnego. Ten rodzaj optymalizacji jest znany jako fuzja lub wylesianie w funkcjonalnych językach programowania i może znacznie poprawić wydajność programu.

Jedyny problem, jaki mam z funkcjami generatora Bergi, polega na tym, że nie są one specyficzne dla Mapobiektów. Zamiast tego są uogólnione dla wszystkich iterowalnych obiektów. Dlatego zamiast wywoływać funkcje zwrotne z (value, key)parami (jak spodziewałbym się przy mapowaniu na a Map), wywołuje funkcje zwrotne z (value, index)parami. W przeciwnym razie jest to doskonałe rozwiązanie i zdecydowanie polecam używanie go zamiast rozwiązań, które dostarczyłem.

Oto konkretne funkcje generatora, których użyłbym do mapowania i filtrowania Mapobiektów:

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

Można ich używać w następujący sposób:

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>

Jeśli chcesz mieć bardziej płynny interfejs, możesz zrobić coś takiego:

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>

Mam nadzieję, że to pomoże.

Aadit M Shah
źródło
robi dzięki! Nadanie dobrej odpowiedzi @Bergi, ponieważ nie znałem „Array.from” i to jest najbardziej trafna odpowiedź. Ale też bardzo interesująca dyskusja między wami!
Stefano
1
@Stefano Zredagowałem swoją odpowiedź, aby pokazać, jak można używać generatorów do poprawnego przekształcania Mapobiektów przy użyciu wyspecjalizowanych mapi filterfunkcji. Odpowiedź Bergiego demonstruje użycie typów ogólnych mapi filterfunkcji dla wszystkich obiektów iterowalnych, których nie można użyć do przekształcania Mapobiektów, ponieważ klucze Mapobiektu zostały utracone.
Aadit M Shah
Wow, naprawdę podoba mi się twój montaż. Skończyło się na tym, że napisałem własną odpowiedź tutaj: stackoverflow.com/a/28721418/422670 (dodane tam, ponieważ to pytanie zostało zamknięte jako duplikat), ponieważ Array.fromnie działa w Chrome (podczas gdy mapa i iteratory tak!). Ale widzę, że podejście jest bardzo podobne i możesz po prostu dodać funkcję „toArray” do swojej grupy!
Stefano
1
@Stefano Rzeczywiście. Zredagowałem odpowiedź, aby pokazać, jak dodać toArrayfunkcję.
Aadit M Shah
7

Mała aktualizacja z 2019 roku:

Teraz Array.from wydaje się być powszechnie dostępny, a ponadto przyjmuje drugi argument mapFn , który uniemożliwia mu utworzenie pośredniej tablicy. Zasadniczo wygląda to tak:

Array.from(myMap.entries(), entry => {...});
nromaniv
źródło
ponieważ odpowiedź z Array.fromjuż istnieje, bardziej nadaje się jako komentarz lub żądana edycja tej odpowiedzi ... ale dzięki!
Stefano
1

Możesz użyć biblioteki, takiej jak https://www.npmjs.com/package/itiriri, która implementuje metody podobne do tablic dla iterable:

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
}
dimadeveatii
źródło
Ta biblioteka wygląda niesamowicie i brakuje jej połączenia, aby przeskoczyć do iterable @dimadeveatii - dziękuję bardzo za jej napisanie, spróbuję wkrótce :-)
Angelos Pikoulas
0

Możesz uzyskać tablicę tablic (klucz i wartość):

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

Następnie możesz łatwo uzyskać wartości od wewnątrz, na przykład klucze z iteratorem mapy.

[...this.state.selected[asd].entries()].map(e=>e[0])
//(2) [2, 3]
ValRob
źródło
0

Możesz również użyć płynnej iteracji, aby przekształcić w tablicę:

const iterable: Iterable<T> = ...;
const arr: T[] = fluent(iterable).toArray();
kataik
źródło