Dlaczego obiekty nie są iterowalne w JavaScript?

87

Dlaczego obiekty nie są domyślnie iterowalne?

Cały czas widzę pytania związane z iteracją obiektów, a typowym rozwiązaniem jest iteracja właściwości obiektu i uzyskiwanie w ten sposób dostępu do wartości w obiekcie. Wydaje się to tak powszechne, że zastanawiam się, dlaczego same obiekty nie są iterowalne.

Instrukcje, takie jak ES6 for...of, przydałyby się domyślnie dla obiektów. Ponieważ te funkcje są dostępne tylko dla specjalnych „iterowalnych obiektów”, które nie zawierają {}obiektów, musimy przejść przez obręcze, aby to zadziałało dla obiektów, do których chcemy ich użyć.

Instrukcja for ... of tworzy pętlę przechodzącą przez iterowalne obiekty (w tym Array, Map, Set, arguments object i tak dalej) ...

Na przykład używając funkcji generatora 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);
  }
}

Powyższe poprawnie rejestruje dane w kolejności, w jakiej się tego spodziewam, gdy uruchamiam kod w przeglądarce Firefox (która obsługuje ES6 ):

wyjście hacky dla ... z

Domyślnie {}obiekty nie są iterowalne, ale dlaczego? Czy wady przewyższałyby potencjalne korzyści wynikające z iterowalności obiektów? Jakie są problemy z tym związane?

Ponadto, ponieważ {}przedmioty różnią się od „Array-podobny” i „iterowalny zbiorów obiektów”, takie jak NodeList, HtmlCollectioni argumentsnie mogą one być przekształcone w tablicach.

Na przykład:

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

lub być używane z metodami Array:

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

Oprócz pytań, które mam powyżej, chciałbym zobaczyć działający przykład, jak przekształcić {}obiekty w iterowalne, szczególnie od tych, którzy wspomnieli o [Symbol.iterator]. Powinno to pozwolić tym nowym {}„iterowalnym obiektom” na używanie takich instrukcji jak for...of. Zastanawiam się również, czy uczynienie obiektów iterowalnymi pozwala na przekształcenie ich w tablice.

Wypróbowałem poniższy kod, ale otrzymałem 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
źródło
1
Wszystko, co ma Symbol.iteratorwłaściwość, jest iterowalne. Musisz więc po prostu zaimplementować tę właściwość. Jedno możliwe wytłumaczenie dlaczego obiekty są nie mógł być iterable że oznaczałoby to wszystko było iterable, ponieważ wszystko jest obiektem (z wyjątkiem prymitywów oczywiście). Jednak co to znaczy iterować po funkcji lub obiekcie wyrażenia regularnego?
Felix Kling
7
Jakie jest twoje aktualne pytanie? Dlaczego ECMA podjęła takie decyzje?
Steve Bennett
3
Ponieważ obiekty NIE mają gwarantowanej kolejności swoich właściwości, zastanawiam się, czy to wyłamuje się z definicji iterowalnej, której można by oczekiwać, że będzie miał przewidywalny porządek?
jfriend00
2
Aby uzyskać miarodajną odpowiedź „dlaczego”, należy zapytać na stronie esdiscuss.org
Felix Kling
1
@FelixKling - czy to post o ES6? Prawdopodobnie powinieneś go edytować, aby powiedzieć, o jakiej wersji mówisz, ponieważ „nadchodząca wersja ECMAScript” nie działa zbyt dobrze w czasie.
jfriend00

Odpowiedzi:

42

Spróbuję. Zauważ, że nie jestem powiązany z ECMA i nie mam wglądu w ich proces podejmowania decyzji, więc nie mogę definitywnie powiedzieć, dlaczego oni coś zrobili lub nie zrobili. Jednak przedstawię swoje założenia i zrobię co w mojej mocy.

1. Po co w ogóle dodawać for...ofkonstrukcję?

JavaScript zawiera już for...inkonstrukcję, której można użyć do iteracji właściwości obiektu. Jednak tak naprawdę nie jest to pętla forEach , ponieważ wylicza wszystkie właściwości obiektu i zwykle działa przewidywalnie tylko w prostych przypadkach.

Psuje się w bardziej złożonych przypadkach (w tym w przypadku tablic, gdzie jego użycie jest zwykle zniechęcane lub całkowicie zaciemniane przez zabezpieczenia potrzebne do prawidłowego użycia for...inz macierzą ). Możesz to obejść, używając (między innymi), ale jest to trochę niezgrabne i nieeleganckie. hasOwnProperty

Dlatego zakładam, że for...ofkonstrukcja jest dodawana w celu wyeliminowania braków związanych z for...inkonstrukcją i zapewnienia większej użyteczności i elastyczności podczas iteracji. Ludzie mają tendencję do traktowania for...injako forEachpętli, które mogą być powszechnie stosowane do jakichkolwiek wyników zbiórki i produkty w rozsądnych ewentualnego związku, ale to nie to, co się dzieje. Te for...ofpoprawki pętlę.

Zakładam również, że ważne jest, aby istniejący kod ES5 działał pod ES6 i dawał taki sam wynik jak w ES5, więc nie można wprowadzić istotnych zmian, na przykład w zachowaniu for...inkonstrukcji.

2. Jak to for...ofdziała?

Dokumentacja referencyjna jest przydatna w tej części. W szczególności rozważany jest obiekt, iterablejeśli definiuje on Symbol.iteratorwłaściwość.

Definicja właściwości powinna być funkcją, która zwraca elementy w kolekcji, jeden po drugim, i ustawia flagę wskazującą, czy jest więcej elementów do pobrania. Wstępnie zdefiniowane implementacje są dostępne dla niektórych typów obiektów i jest stosunkowo jasne, że użycie for...ofprostych delegatów do funkcji iteratora.

Takie podejście jest przydatne, ponieważ bardzo ułatwia dostarczanie własnych iteratorów. Mogę powiedzieć, że podejście to mogło spowodować problemy praktyczne ze względu na oparcie się na zdefiniowaniu nieruchomości, której wcześniej nie było, z wyjątkiem tego, co mogę powiedzieć, że tak nie jest, ponieważ nowa nieruchomość jest zasadniczo ignorowana, chyba że celowo jej szukasz (tj. nie będzie prezentował się w for...inpętlach jako klucz itp.). Więc tak nie jest.

Odkładając na bok kwestie praktyczne, rozpoczęcie wszystkich obiektów od nowej, predefiniowanej właściwości lub pośrednie stwierdzenie, że „każdy obiekt jest zbiorem” mogło być koncepcyjnie kontrowersyjne.

3. Dlaczego obiekty nie iterableużywając for...ofdomyślnie?

Moje przypuszczenie to, że jest to połączenie:

  1. iterableDomyślne ustawienie wszystkich obiektów mogło zostać uznane za niedopuszczalne, ponieważ dodaje właściwość, której wcześniej nie było, lub ponieważ obiekt nie jest (koniecznie) kolekcją. Jak zauważa Felix, „co to znaczy iterować po funkcji lub obiekcie wyrażenia regularnego”?
  2. Proste obiekty można już iterować przy użyciu for...ini nie jest jasne, co mogłaby zrobić wbudowana implementacja iteratora inaczej / lepiej niż istniejące for...inzachowanie. Więc nawet jeśli nr 1 jest błędny i dodanie właściwości było do zaakceptowania, mogło nie zostać uznane za przydatne .
  3. Użytkownicy, którzy chcą tworzyć swoje obiekty, iterablemogą to łatwo zrobić, definiując Symbol.iteratorwłaściwość.
  4. Specyfikacja ES6 zapewnia również typ mapy , który jest iterable domyślny i ma kilka innych niewielkich zalet w porównaniu z użyciem zwykłego obiektu jako pliku Map.

W dokumentacji referencyjnej podano nawet przykład nr 3:

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

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

Biorąc pod uwagę, że obiekty można łatwo tworzyć iterable, że można je już iterować przy użyciu for...ini że prawdopodobnie nie ma jasnej zgody co do tego, co powinien robić domyślny iterator obiektów (jeśli to, co robi, ma się w jakiś sposób różnić od tego, co for...inrobi), wydaje się rozsądne wystarczy, że obiekty nie zostały utworzone iterabledomyślnie.

Zwróć uwagę, że Twój przykładowy kod można przepisać za pomocą 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"
    }
}

... lub możesz również stworzyć swój obiekt iterabletak, jak chcesz, wykonując coś podobnego do następującego (lub możesz utworzyć wszystkie obiekty iterable, przypisując Object.prototype[Symbol.iterator]zamiast tego):

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

Możesz się tym bawić tutaj (w Chrome, przynajmniej): http://jsfiddle.net/rncr3ppz/5/

Edytować

W odpowiedzi na zaktualizowane pytanie tak, można przekonwertować an iterablena tablicę, używając operatora spreadu w ES6.

Jednak wydaje się, że to jeszcze nie działa w Chrome, a przynajmniej nie mogę go uruchomić w moim jsFiddle. W teorii powinno być tak proste, jak:

var array = [...myIterable];
aroth
źródło
Dlaczego nie zrobić tego obj[Symbol.iterator] = obj[Symbol.enumerate]w ostatnim przykładzie?
Bergi
@Bergi - Ponieważ nie widziałem tego w dokumentach (i nie widzę opisanej tutaj właściwości ). Chociaż jednym argumentem przemawiającym za jawnym zdefiniowaniem iteratora jest to, że ułatwia on egzekwowanie określonej kolejności iteracji, jeśli jest to wymagane. Jeśli kolejność iteracji nie jest ważna (lub jeśli domyślna kolejność jest w porządku), a skrót jednowierszowy działa, nie ma powodu, aby nie przyjmować bardziej zwięzłego podejścia.
aroth
Ups, [[enumerate]]nie jest dobrze znanym symbolem (@@ wyliczenie), ale metodą wewnętrzną. Musiałbym byćobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi
Jaki pożytek mają te wszystkie domysły, skoro sam proces dyskusji jest dobrze udokumentowany? To bardzo dziwne, że powiesz: „Dlatego zakładam, że konstrukt for ... of jest dodawany w celu usunięcia braków związanych z konstrukcją for ... in”. Nie. Został dodany, aby wspierać ogólny sposób iterowania wszystkiego i jest częścią szerokiego zestawu nowych funkcji, w tym samych elementów iteracyjnych, generatorów oraz map i zestawów. Nie ma na celu zastąpienia lub uaktualnienia for...in, co ma inny cel - iterację we właściwościach obiektu.
2
Warto jeszcze raz podkreślić, że nie każdy przedmiot jest zbiorem. Obiekty jako takie były używane od dawna, bo było to bardzo wygodne, ale ostatecznie nie są to tak naprawdę kolekcje. Na razie to mamy Map.
Felix Kling
9

Objects nie implementują protokołów iteracji w Javascript z bardzo ważnych powodów. Istnieją dwa poziomy, na których można iterować właściwości obiektu w JavaScript:

  • poziom programu
  • poziom danych

Iteracja na poziomie programu

Kiedy iterujesz obiekt na poziomie programu, badasz część struktury swojego programu. Jest to operacja refleksyjna. Zilustrujmy tę instrukcję typem tablicy, który jest zwykle powtarzany na poziomie danych:

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

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

Właśnie zbadaliśmy poziom programu xs. Ponieważ tablice przechowują sekwencje danych, regularnie interesuje nas tylko poziom danych. for..inewidentnie nie ma sensu w połączeniu z tablicami i innymi strukturami „zorientowanymi na dane” w większości przypadków. To jest powód, dla którego ES2015 wprowadził iterowalny for..ofprotokół.

Iteracja poziomu danych

Czy to oznacza, że ​​możemy po prostu odróżnić dane od poziomu programu, odróżniając funkcje od typów pierwotnych? Nie, ponieważ funkcje mogą być również danymi w JavaScript:

  • Array.prototype.sort na przykład oczekuje, że funkcja wykona określony algorytm sortowania
  • Thunks jak () => 1 + 2są tylko funkcjonalnymi opakowaniami dla leniwie ocenianych wartości

Oprócz wartości pierwotnych mogą również reprezentować poziom programu:

  • [].lengthna przykład jest a Numberale reprezentuje długość tablicy, a zatem należy do dziedziny programu

Oznacza to, że nie możemy odróżnić programu od poziomu danych, po prostu sprawdzając typy.


Ważne jest, aby zrozumieć, że implementacja protokołów iteracji dla zwykłych starych obiektów Javascript opierałaby się na poziomie danych. Ale jak właśnie widzieliśmy, wiarygodne rozróżnienie między danymi a iteracją na poziomie programu nie jest możliwe.

W przypadku Arrays to rozróżnienie jest trywialne: każdy element z kluczem liczb całkowitych jest elementem danych. Objects mają równoważną cechę: enumerabledeskryptor. Ale czy naprawdę warto na tym polegać? Myślę, że tak nie jest! Znaczenie enumerabledeskryptora jest zbyt niewyraźne.

Wniosek

Nie ma sensownego sposobu implementacji protokołów iteracji dla obiektów, ponieważ nie każdy obiekt jest kolekcją.

Jeśli właściwości obiektu były domyślnie iterowalne, pomieszano poziom programu i danych. Ponieważ każdy rodzaj kompozytu w JavaScript jest oparte na prostych obiektów byłoby to ubiegać Array, a Maptakże.

for..in, Object.keys, Reflect.ownKeysItd. Mogą być używane zarówno do refleksji i danych iteracji regularnie wyraźne rozróżnienie nie jest możliwe. Jeśli nie jesteś ostrożny, szybko skończysz z metaprogramowaniem i dziwnymi zależnościami. MapAbstrakcyjny typ danych skutecznie kończy utożsamiając programu i poziomu danych. Uważam, że Mapjest to najważniejsze osiągnięcie ES2015, nawet jeśli Promisesą one dużo bardziej ekscytujące.


źródło
3
+1, myślę: „Nie ma sensownego sposobu implementacji protokołów iteracji dla obiektów, ponieważ nie każdy obiekt jest zbiorem”. podsumowuje.
Charlie Schliesser
1
Nie sądzę, żeby to był dobry argument. Jeśli twój obiekt nie jest kolekcją, dlaczego próbujesz nad nim zapętlić? Nie ma znaczenia, że nie każdy obiekt jest kolekcją, ponieważ nie będziesz próbować iterować po tych, które nie są.
BT,
W rzeczywistości każdy obiekt jest zbiorem i to nie język decyduje, czy zbiór jest spójny, czy nie. Tablice i mapy mogą również zbierać niepowiązane wartości. Chodzi o to, że możesz iterować po kluczach dowolnego obiektu niezależnie od ich użycia, więc jesteś o jeden krok od iteracji po ich wartościach. Jeśli mówisz o języku, który statycznie wpisuje wartości tablicowe (lub dowolne inne zbiory), możesz mówić o takich ograniczeniach, ale nie o JavaScript.
Manngo,
Argument, że każdy obiekt nie jest kolekcją, nie ma sensu. Zakładasz, że iterator ma tylko jeden cel (iteracja kolekcji). Domyślnym iteratorem obiektu byłby iterator właściwości obiektu, bez względu na to, co te właściwości reprezentują (czy to kolekcja, czy coś innego). Jak powiedział Manngo, jeśli twój obiekt nie reprezentuje kolekcji, to od programisty zależy, czy nie będzie traktował go jak kolekcji. Może chcą po prostu powtórzyć właściwości obiektu w celu uzyskania wyników debugowania? Poza kolekcją istnieje wiele innych powodów.
jfriend00
8

Wydaje mi się, że pytanie powinno brzmieć „dlaczego nie ma wbudowanej iteracji obiektu?

Dodanie iterowalności do samych obiektów może mieć niezamierzone konsekwencje i nie, nie ma sposobu na zagwarantowanie porządku, ale napisanie iteratora jest tak proste, jak

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

Następnie

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

a 1
b 2

źródło
1
dzięki torazaburo. poprawiłem swoje pytanie. Bardzo chciałbym zobaczyć przykład, [Symbol.iterator]w którym można rozwinąć te niezamierzone konsekwencje.
boombox
4

Możesz łatwo uczynić wszystkie obiekty iterowalnymi globalnie:

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
źródło
3
Nie dodawaj globalnie metod do obiektów natywnych. To okropny pomysł, który ugryzie ciebie i każdego, kto używa twojego kodu, w dupę.
BT,
2

To jest najnowsze podejście (które działa w chrome canary)

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

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

tak entries jest teraz metodą, która jest częścią Object :)

edytować

Po dokładniejszym przyjrzeniu się temu wydaje się, że możesz wykonać następujące czynności

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

więc możesz teraz to zrobić

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

edytuj 2

Wracając do oryginalnego przykładu, gdybyś chciał użyć powyższej metody prototypowej, chciałby tak

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
źródło
2

Mnie też przeszkadzało to pytanie.

Wtedy wpadłem na pomysł użycia Object.entries({...}), zwraca wartość, Arrayktóra jestIterable .

Dr Axel Rauschmayer zamieścił również doskonałą odpowiedź na ten temat. Zobacz Dlaczego zwykłe obiekty NIE są iterowalne

Romko
źródło
0

Technicznie nie jest to odpowiedź na pytanie, dlaczego? ale dostosowałem powyższą odpowiedź Jacka Slocuma w świetle komentarzy BT do czegoś, co może być użyte do uczynienia obiektu iterowalnym.

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

Nie tak wygodne, jak powinno, ale jest wykonalne, zwłaszcza jeśli masz wiele obiektów:

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

A ponieważ każdy ma prawo do opinii, nie rozumiem, dlaczego obiekty nie są już iterowalne. Jeśli możesz, wypełnij je jak powyżej lub użyjfor … in , nie widzę prostego argumentu.

Jedną z możliwych sugestii jest to, że to, co jest iterowalne, to typ obiektu, więc możliwe jest, że iterowalność została ograniczona do podzbioru obiektów, na wypadek gdyby niektóre inne obiekty eksplodowały podczas próby.

Manngo
źródło