ES6 – iterators

javascript

W kolejnym wpisie na temat ECMAScript2015 przedstawię Wam sposób na budowę i użycie własnych iteratorów, które możemy porównać do tych znanych z Javy (Iterable) lub .NET (IEnumerable). ES6 to również wbudowane iteratory dla typów: String i Array oraz nowych: TypedArray, Map i Set.


Zobacz całą serię: Let-s talk about ECMAScript 2015


pętla for-of

Zanim zacznę opisywać budowanie własnych iteratorów, warto nadmienić, że bezpośrednio z nimi związana jest nowa pętla for-of. Powstała ona właśnie po to by obsługiwać iteratory budowane oraz te napisane przez nas.

Jej sygnatura to:

for (LET of ITERABLE) {
  CODE BLOCK
}

Jest ona bardzo podobna do pętli for-in, służącej iterowaniu po własnościach obiektów. Przykładowo, kolekcje typu Array posiadają od teraz wbudowany iterator, który pozwala na dostęp do kolejnych elementów:

const arr = [1, 2, 3, 4, 5];
for (let item of arr) {
  console.log(item); // 1
                     // 2
                     // 3
                     // 4
                     // 5
}

Wraz z pętlą for-of możemy również użyć: breakcontinue oraz return.

const arr = [1, 2, 3, 4, 5];
for (let item of arr) {
  if (item > 4) {
    break;
  }
  if (0 !== item % 2) {
    continue;
  }
  console.log(item); // 2
                     // 4
}

Symbol

Kolejnym elementem ECMAScript2015, o którym warto wspomnieć przed przejściem do części dotyczącej iteratora jest Symbol. Jest to niezmienna (immutable) struktura danych, która może nam posłużyć jako identyfikator obiektu. Symbol może posiadać opis, ale jest to element opcjonalny.

// Symbol
let s1 = Symbol('abc');
let s2 = Symbol('abc');
console.log(s1 !== s2); // true
console.log(typeof s1); // 'symbol'
let obj = {};
obj[s1] = 'abc';
console.log(obj); // Object { Symbol(abc): 'abc' }

iterator

Nieprzypadkowo wspomniałem wyżej o strukturze danych Symbol. By stworzyć swój własny iterator, musimy zbudować obiekt, którego identyfikatorem będzie [Symbol.iterator](). Będzie nam potrzebna jeszcze metoda next(), która “wołana” jest jest przez pętlę for-of w celu pobrania kolejnej wartości.

Spójrzmy na przykład prostego iteratora, który będzie zwracać wartości od 1 do 10, a ich ilość zależeć będzie od przekazanego parametru:

let random1_10 = function (items = 1) {
  return {
    [Symbol.iterator]() {
      let cur = 0;
      return {
        next() {
          let done = cur++ === items,
              random = Math.floor(Math.random() * 10) + 1;
          return {
            done: done,
            value: random
          }
        }
      }
    }
  };
};

for (let n of random1_10(5)) {
  console.log(n); // prints 5 random numbers
}

Iterator zwraca dynamicznie generowane numery, zwracając przy tym obiekt:

return {
  done: [Boolean],
  value: [Any]
}

gdzie wartość done, to false lub true, w zależności od tego, czy iterator powinien zakończyć już swoje działanie. W naszym przypadku koniec nastąpi w momencie, gdy zmienna cur osiągnie taką samą wartość, jak parametr items. Wartością value jest dowolny typ danych, który zwracany jest w ciele pętli for-of.

W momencie, gdy wartość dla done, to true, możemy pominąć własność value w zwracanym obiekcie:

let random1_10 = function (items = 1) {
  return {
    [Symbol.iterator]() {
      let cur = 0;
      return {
        next() {
          if (cur++ === items) {
            return {
              done: true 
            }
          }

          return {
            done: false,
            value: Math.floor(Math.random() * 10) + 1
          }
        }
      }
    }
  };
};

for (let n of random1_10(5)) {
 console.log(n); // prints 5 random numbers
}