Iteratory i pętla for-of

> Dodaj do ulubionych

ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawią się w 6. edycji standardu ECMAScript, w skrócie ES6.

Jak za pomocą pętli przejrzeć elementy tablicy w JavaScript? Kiedy język JavaScript pojawił się 20 lat temu, robiło się to następująco:

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

Od czasu ES5 można w tym celu wykorzystać wbudowaną metodę forEach:

myArray.forEach(function (value) {
  console.log(value);
});

To nieco krótsze rozwiązanie, które ma jednak małą wadę: nie ma możliwości przerwania pętli instrukcją break ani wyjścia z wywołanej funkcji za pomocą instrukcji return.

Oczywiście dobrze byłoby, gdyby można było przejrzeć elementy tablicy, posługując się jedynie pętlą for.

A może by tak spróbować z pętlą for–in?

for (var index in myArray) {    // nie rób tego
  console.log(myArray[index]);
}

Nie jest to najlepszy pomysł z kilku powodów:

  • W kodzie tym zmiennej index będą przypisywane nie liczby, lecz łańcuchy: "0", "1", "2" itd. Nie chcemy raczej wykonywać działań arytmetycznych na łańcuchach ("2" + 1 == "21"), zatem rozwiązanie to jest co najmniej niewygodne.
  • Treść pętli zostanie wykonana nie tylko dla elementów tablicy, lecz także wszystkich innych własności, które mogły zostać dodane. Na przykład jeśli tablica w JavaScript ma policzalną własność myArray.name, to wówczas pętla zostanie wykonana jeden dodatkowy raz z instrukcją index == "name". Pętla może zostać wykonana nawet dla własności z łańcucha prototypów tablicy.
  • Co jednak najbardziej zaskakujące, powyższa pętla może przejrzeć elementy tablicy w losowej kolejności.

Krótko rzecz ujmując, pętla for–in została zaprojektowana pod kątem starych, prostych obiektów z kluczami łańcuchowymi. Jeśli chodzi o tablice, nie jest to dobra wiadomość.

Potęga pętli for-of

Wcześniej obiecałem, że ES6 nie wpłynie negatywnie na działanie już napisanego kodu JS. Miliony stron internetowych wykorzystuje pętle for–in – także te operujące na tablicach. W związku z tym w grę nigdy nie wchodziło „naprawienie” pętli for–in pod kątem tablic. Zatem jedynym możliwym usprawnieniem w ES6 było wprowadzenie nowej pętli.

Oto ona:

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

Hmm. Po tym co zdążyliśmy już powiedzieć nie wygląda to zbyt imponująco, prawda? Sprawdźmy czy pętla for–of ma w zanadrzu jeszcze jakieś udogodnienia. Na tę chwilę pamiętaj tylko o tym, że:

  • powyższe rozwiązanie stanowi najzwięźlejszą i najprostszą pętlę operującą na elementach tablic,
  • pozbawione jest ono pułapek związanych z pętlą for–in,
  • w przeciwieństwie do metody forEach(), dobrze współpracuje z instrukcjami break, continue i return.

Pętla for–in przeznaczona jest do przeglądania własności obiektów.

Pętla for–of służy przeglądaniu danych – takich jak wartości umieszczone w tablicy.

To jednak nie wszystko.

Inne kolekcje działające z pętlą for-of

Pętla for–of nie jest przeznaczona tylko dla tablic. Można ją również wykonywać na obiektach przypominających tablice, takich jak obiekty NodeList w modelu DOM.

Ponadto pętla może operować na łańcuchach, które traktowane są jak sekwencje znaków Unicode:

for (var chr of "??") {
  alert(chr);
}

Można ją również wykonywać na obiektach Map i Set.

Ach, wybacz. Nie słyszałeś nigdy o obiektach Map i Set? To nowe elementy w standardzie ES6. Poświęcę im kiedyś cały osobny artykuł. Ich funkcjonowanie nie będzie dla ciebie zaskoczeniem, jeśli znasz słowniki i zbiory z innych języków programowania.

Przykładowo obiekt Set jest przydatny w usuwaniu duplikatów:

// utwórz zbiór z tablicy słów
var uniqueWords = new Set(words);

Gdy już utworzysz obiekt Set, możesz przejrzeć jego zawartość za pomocą pętli. To proste:

for (var word of uniqueWords) {
  console.log(word);
}

Z obiektem Map rzecz ma się nieco inaczej: zawarte w tym obiekcie dane składają się z par klucz-wartość, zatem należy posłużyć się destrukturyzacją, aby wypakować klucze i wartości do dwóch osobnych zmiennych:

for (var [key, value] of phoneBookMap) {
  console.log("Numer telefonu użytkownika" + key + ": " + value);
}

Destrukturyzacja to kolejna z nowości w ES6, która świetnie nadałaby się na osobny artykuł. Muszę to sobie zapisać.

Sytuacja zatem wygląda następująco: w JS jest już sporo różnych klas kolekcyjnych, a będzie ich jeszcze więcej. Pętla for–of została zaprojektowana tak, by służyć jako uniwersalna instrukcja pętlowa, którą będzie można wykonywać dla wszystkich takich klas.

Pętla for–of nie działa ze zwykłymi starymi obiektami, lecz jeśli chcesz przejrzeć iteracyjnie własności obiektu, możesz skorzystać z pętli for–in (która do tego właśnie służy) bądź z wbudowanej funkcji Object.keys():

// zrzuć policzalne własności obiektu do konsoli
for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

Prawdziwe oblicze ES6

Dobrzy artyści kopiują, wielcy kradną — Pablo Picasso

W standardzie ES6 żadna nowość nie wzięła się znikąd. Większość dodatków sprawdziła się już w innych językach programowania.

Na przykład pętla for–of przypomina analogiczne instrukcje pętlowe z języków C++, C#, Java czy Python. Podobnie jak one może operować na kilku różnych strukturach danych dostępnych w języku i jego bibliotece standardowej. Dodatkowo stanowi ona punkt rozszerzenia języka.

Tak jak instrukcje for i foreach w tych innych językach, pętla for–of działa wyłącznie na zasadzie wywołania metody. Tym, co łączy obiekty Array, Map, Set i inne, o których zdążyliśmy nadmienić, to metoda iteracyjna.

Metodę iteracyjną może ponadto mieć jeszcze jeden rodzaj obiektu: dowolnie wybrany obiekt.

Podobnie jak dodanie mójObiekt.toString() do jakiegokolwiek obiektu sprawia, że JS od razu wie jak przekonwertować dany obiekt na łańcuch znaków, dodanie metody mójObiekt[Symbol.iterator]() do dowolnego obiektu sprawi, że JS bez problemu wykona na nim pętlę.

Załóżmy na przykład, że korzystasz z jQuery i choć lubisz metodę .each(), to chciałbyś by obiekty jQuery można było przejrzeć także za pomocą pętli for–of. Robi się to tak:

// obiekty jQuery przypominają tablice,
// zatem wywołujemy na nich tę samą metodę iteracyjną co na tablicach
jQuery.prototype[Symbol.iterator] =
  Array.prototype[Symbol.iterator];

Dobra, wiem co sobie teraz myślisz. Składnia [Symbol.iterator] wydaje się dosyć dziwna. Co tu się dzieje? Wszystko to ma związek z nazwą metody. Komisja standaryzacyjna mogła po prostu nazwać tę metodę .iterator(), ale wówczas moglibyśmy mieć do czynienia z sytuacją, w której istniejący już kod zawierałby obiekty z metodami o nazwie .iterator(), a to doprowadziłoby do sporego zamieszania. W standardzie powyższa metoda nie ma zatem nazwy, lecz symbol.

Symbole to nowy element w ES6, o którym powiemy sobie – tak, zgadłeś – w jednym z kolejnych artykułów. Na tę chwilę pamiętaj jedynie, że w standardzie można zdefiniować zupełnie nowy symbol, np. Symbol.iterator, który na pewno nie będzie powodował konfliktów z istniejącym już kodem. Takie kompromisowe rozwiązanie sprawia, że składnia staje się nieco dziwna, lecz jest to niewielka cena jak za tak wszechstronny nowy składnik funkcjonalności i zgodność ze starym kodem.

Obiekty z metodą [Symbol.iterator]() nazywamy obiektami iterowalnymi. W najbliższych tygodniach przekonamy się, że iterowalne obiekty to pojęcie używane na przestrzeni całego języka – nie tylko w kontekście pętli for–of, lecz także konstruktorów Map i Set, przypisania destrukturyzacyjnego i nowego operatora rozszczepiania (ang. spread operator).

Obiekt iteratora

Możliwe że nigdy nie będziesz musiał zaimplementować własnego obiektu iteratora kompletnie od zera. Dlaczego? O tym powiemy w innym artykule z serii. Aby jednak zupełnie nie urywać tematu, zobaczmy jak wygląda obiekt iteratora. (Jeśli pominiesz całą poniższą część artykułu, stracisz głównie szczegóły techniczne).

Pętla for–of rozpoczyna się wywołaniem metody [Symbol.iterator]() na wybranej kolekcji. W wyniku wywołania zwracany jest nowy obiekt iteratora. Obiektem iteratora może być dowolny obiekt z metodą .next(). Pętla for–of wywoła metodę wielokrotnie, raz na każdą iterację. Oto przykład najprostszego obiektu iteratora, jaki jestem w stanie sobie wyobrazić:

var zeroesForeverIterator = {
  [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};

Po każdym wywołaniu metody .next() zwracany jest ten sam wynik, który przekazuje pętli for–of, że (a) iteracja nie dobiegła jeszcze końca, i że (b) kolejną wartością jest 0. Pętla for (value of zeroesForeverIterator) {} będzie więc nieskończona. Oczywiście typowe iteratory nie są tak proste.

Funkcjonowanie takiego iteratora, mającego własności .done i .value, różni się od iteratorów w innych językach. W Javie iteratory mają osobne metody .hasNext() i .next(). W Pythonie natomiast istnieje pojedyncza metoda .next(), która zgłasza wyjątek StopIteration po wyczerpaniu się wszystkich wartości. We wszystkich trzech przypadkach zwracane są jednak właściwie te same informacje.

Obiekt iteratora może również implementować opcjonalne metody .return() i .throw(wyjątek) . Pętla for–of wywoła metodę .return(), jeśli do zakończenia pętli doszło przedwcześnie w wyniku wyjątku lub instrukcji break bądź return. Iterator może implementować metodę .return(), jeśli potrzebne jest zwolnienie wykorzystywanych wcześniej zasobów. Większość iteratorów nie musi jej jednak implementować. Szczególny przypadek stanowi natomiast metoda .throw(wyjątek) , bowiem pętla for–of nigdy jej nie wywołuje. Więcej o tym powiemy w innym artykule z serii.

Skoro poznaliśmy już wszystkie szczegóły, weźmy prostą pętlę for–of i przepiszmy ją tak, by zobaczyć wywoływane przez nią metody.

Na początek pętla for–of:

for (VAR of ITERABLE) {
  STATEMENTS
}

Oto mniej więcej jakie metody kryją się za naszą pętlą, plus kilka zmiennych tymczasowych:


var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  VAR = $result.value;
  STATEMENTS
  $result = $iterator.next();
}

Powyższy kod nie pokazuje, jak obsługiwana jest metoda .return(). Moglibyśmy dodać taki fragment, lecz zamazałby on nam obraz funkcjonowania pętli, a zależy nam na klarowności przykładu. Pętla for–of jest łatwa w użyciu, lecz kryje się za nią wiele operacji.

Kiedy będę mógł zacząć korzystać z pętli for–of

Pętla for–of obsługiwana jest we wszystkich aktualnych wersjach przeglądarki Firefox, a także w przeglądarce Chrome po wejściu na adres chrome://flags i włączeniu opcji Włącz eksperymentalny JavaScript. Działa także w przeglądarce Spartan Microsoftu, ale w oficjalnych wersjach IE już nie. Jeśli chcesz korzystać z nowej składni w przeglądarkach, a twój kod musi funkcjonować w IE i Safari, możesz użyć kompilatora Babel lub Traceur firmy Google, który przetłumaczy kod w standardzie ES6 na bardziej przyjazny przeglądarkom ES5.

Na serwerze nie będziesz potrzebować kompilatora – już teraz możesz korzystać z pętli for–of w środowisku io.js (oraz Node, z opcją --harmony).

{done: true}

Uff!

Na dziś to byłoby na tyle, ale wciąż nie wyczerpaliśmy tematu pętli for–of.

W ES6 istnieje bowiem jeszcze jeden rodzaj obiektu, który świetnie współpracuje z tą pętlą. Nie wspominałem o nim, ponieważ będzie on tematem kolejnego artykułu. Moim zdaniem jest to najbardziej niezwykła nowość w ES6. Jeśli nie spotkałeś się z nią dotychczas w językach takich jak Python czy C#, jej zrozumienie może z początku wydać ci się trudne. Nie ma jednak łatwiejszego sposobu na napisanie iteratora. Funkcjonalność ta przydaje się także w refaktoryzacji i niewykluczone, że zmieni sposób, w jaki piszemy kod asynchroniczny – zarówno ten przeglądarkowy, jak i działający po stronie serwera. Odwiedzaj nas zatem często, by szczegółowo poznać generatory w ES6.

Autor: Jason Orendorff

Źródło: https://hacks.mozilla.org/2015/04/es6-in-depth-iterators-and-the-for-of-loop/

Tłumaczenie: Joanna Liana

Treść tej strony jest dostępna na zasadach licencji CC BY-SA 3.0