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 ):
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
, HtmlCollection
i arguments
nie 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
Symbol.iterator
wł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?Odpowiedzi:
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...of
konstrukcję?JavaScript zawiera już
for...in
konstrukcję, 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...in
z macierzą ). Możesz to obejść, używając (między innymi), ale jest to trochę niezgrabne i nieeleganckie.hasOwnProperty
Dlatego zakładam, że
for...of
konstrukcja jest dodawana w celu wyeliminowania braków związanych zfor...in
konstrukcją i zapewnienia większej użyteczności i elastyczności podczas iteracji. Ludzie mają tendencję do traktowaniafor...in
jakoforEach
pę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. Tefor...of
poprawki 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...in
konstrukcji.2. Jak to
for...of
działa?Dokumentacja referencyjna jest przydatna w tej części. W szczególności rozważany jest obiekt,
iterable
jeśli definiuje onSymbol.iterator
wł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...of
prostych 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...in
pę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
iterable
używającfor...of
domyślnie?Moje przypuszczenie to, że jest to połączenie:
iterable
Domyś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”?for...in
i nie jest jasne, co mogłaby zrobić wbudowana implementacja iteratora inaczej / lepiej niż istniejącefor...in
zachowanie. 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 .iterable
mogą to łatwo zrobić, definiującSymbol.iterator
właściwość.iterable
domyślny i ma kilka innych niewielkich zalet w porównaniu z użyciem zwykłego obiektu jako plikuMap
.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życiufor...in
i ż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, cofor...in
robi), wydaje się rozsądne wystarczy, że obiekty nie zostały utworzoneiterable
domyś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
iterable
tak, jak chcesz, wykonując coś podobnego do następującego (lub możesz utworzyć wszystkie obiektyiterable
, przypisującObject.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
iterable
na 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];
źródło
obj[Symbol.iterator] = obj[Symbol.enumerate]
w ostatnim przykładzie?[[enumerate]]
nie jest dobrze znanym symbolem (@@ wyliczenie), ale metodą wewnętrzną. Musiałbym byćobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
for...in
, co ma inny cel - iterację we właściwościach obiektu.Map
.Object
s 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: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..in
ewidentnie 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ł iterowalnyfor..of
protokół.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() => 1 + 2
są tylko funkcjonalnymi opakowaniami dla leniwie ocenianych wartościOprócz wartości pierwotnych mogą również reprezentować poziom programu:
[].length
na przykład jest aNumber
ale reprezentuje długość tablicy, a zatem należy do dziedziny programuOznacza 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
Array
s to rozróżnienie jest trywialne: każdy element z kluczem liczb całkowitych jest elementem danych.Object
s mają równoważną cechę:enumerable
deskryptor. Ale czy naprawdę warto na tym polegać? Myślę, że tak nie jest! Znaczenieenumerable
deskryptora 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
, aMap
także.for..in
,Object.keys
,Reflect.ownKeys
Itd. 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.Map
Abstrakcyjny typ danych skutecznie kończy utożsamiając programu i poziomu danych. Uważam, żeMap
jest to najważniejsze osiągnięcie ES2015, nawet jeśliPromise
są one dużo bardziej ekscytujące.źródło
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
[Symbol.iterator]
w którym można rozwinąć te niezamierzone konsekwencje.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]]; } } } });
źródło
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); } }
źródło
Mnie też przeszkadzało to pytanie.
Wtedy wpadłem na pomysł użycia
Object.entries({...})
, zwraca wartość,Array
która jestIterable
.Dr Axel Rauschmayer zamieścił również doskonałą odpowiedź na ten temat. Zobacz Dlaczego zwykłe obiekty NIE są iterowalne
źródło
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żyj
for … 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.
źródło