Protokoły Iterator i Iterable
Jedną z nowości, jakie wprowadzono do ES6 był protokół, a właściwie dwa protokoły iterator
oraz iterable
. Zastanawiasz się do czego może Ci się przydać ich znajomość? Jeśli nie dla zwykłej ciekawości i zgłębiania JS-a, poznając iteratory możesz lepiej zrozumieć jak działa inny mechanizm w JS, a mianowicie generatory.
Protokoły te mogą zostać zaimplementowane przez dowolny obiekt, jeśli ten będzie podążał ustalonymi konwencjami.
Zacznijmy od protokołu iterable
. Pozwala on na zdefiniowanie sposobu w jaki będziemy mogli iterować po obiekcie.
Niektóre z wbudowanych obiektów w JS implementują protokół iterable
, co oznacza tyle, że mają zdefiniowany sposób w jaki możemy po nich iterować np za pomocą pętli for..of
. Jeśli zajrzymy do prototypów typów wbudowanych Array
, String
, Map
czy Set
, zobaczymy, że implementują one metodę @@iterator
- znajdziemy tam klucz Symbol(Symbol.iterator)
:
// W konsoli przeglądarki wpisz
Array.prototype
length: 0
constructor: ƒ Array()
...
Symbol(Symbol.iterator): ƒ values()
arguments: (...)
caller: (...)
length: 0
name: "values"
__proto__: ƒ ()
[[Scopes]]: Scopes[0]
Typ Object domyślnie nie jest iterable
, ale aby taki był, musi implementować metodę @@iterator
czyli ten obiekt, lub jeden z obiektów w łańcuchu prototypowym musi posiadać pole @@iterator
dostepne przez stałą Symbol.iterator
.
Jeśli spróbujemy użyć pętli for..of
na obiekcie, który nie implementuje protokołu iterable, dostaniemy informację, że nasz obiekt nie jest iterable
.
const colors = {
black: '#000',
white: '#fff',
red: '#f00',
green: '#0f0',
blue: '#00f',
};
for (color of colors) {
console.log(color);
}
// W konsoli zobaczymy, że nasz obiekt nie jest typu iterable
Uncaught TypeError: colors is not iterable...
Spróbujmy to naprawić:
colors[Symbol.iterator] = function () {
// Tworzymy tablicę zawierającą wartości
// dla każdego z kluczy obiektu
const colorValues = Object.values(this);
// Licznik przechowujący numer aktualnej iteracji
let iteration = 0;
return {
// zgodnie z protokołem implementujemy metodę next(),
// która zwraca obiekt z polami value i done
next: () =>
iteration < colorValues.length
? // jeśli nadal mamy elementy do pobrania
// zwracamy wartość i ustawiamy done na false
{ value: colorValues[iteration++], done: false }
: // w przeciwnym razie zwracamy done z warotścią false
{ value: undefined, done: true },
};
};
Jeśli teraz użyjemy pętli for..of
:
for (let color of colors) {
console.log(color);
}
// teraz pętla wie jak iterować po naszym obiekcie
('#000');
('#fff');
('#f00');
('#0f0');
('#00f');
Możecie pomyśleć "przecież to samo mogę osiągnąć dużo łatwiej używając Object.values(colors)
". Zgadza się, ale zrobiliśmy to żeby zrozumieć działanie protokołu na prostym przykładzie.
Ciekawszym przykładem może być na przykład iterator, który zwraca klucze obiektu w kolejności alfabetycznej.
colors[Symbol.iterator] = function () {
// Jedyna zmiana:
// wywołujemy metodę sort na tablicy wartości.
const colorKeys = Object.keys(this).sort();
let iteration = 0;
return {
next: () =>
iteration < colorKeys.length
? { value: colorKeys[iteration++], done: false }
: { value: undefined, done: true },
};
};
Zobaczmy jak działa nasz iterator:
for (color of colors) {
console.log(color);
}
// Wynik:
black;
blue;
green;
red;
white;
Jak widać nasz nowy iterator zwrócił klucze w kolejności alfabetycznej.
Jestem ciekawy co sądzicie o opisanych protokołach. Używacie ich w swoich projektach? Może macie pomysł jak je wykorzystać? Dajcie znać w komentarzach, a ja zabieram się do pisania następnego artykuły - tym razem o generatorach, który podlinkuje tu jak tylko będzie gotowy.