24 квітня 2022 р.

Ітеративні об’єкти

Ітеративні об’єкти є узагальненням масивів. Це концепція, яка дозволяє нам зробити будь-який об’єкт придатним для використання в циклі for..of.

Звичайно, по масивах можна ітеруватися. Але є багато інших вбудованих об’єктів, які також можна ітерувати. Наприклад, рядки також можна ітерувати.

Якщо об’єкт технічно не є масивом, а представляє колекцію (list, set) чогось, то for..of – чудовий синтаксис для його обходу, тому подивімось, як змусити його працювати.

Symbol.iterator

Ми можемо легко зрозуміти концепцію ітеративних об’єктів, зробивши її власноруч.

Наприклад, у нас є об’єкт, який не є масивом, але виглядає придатним для for..of.

Як, наприклад, об’єкт range, який представляє інтервал чисел:

let range = {
  from: 1,
  to: 5
};

// Ми хочемо, щоб for..of працював:
// for(let num of range) ... num=1,2,3,4,5

Щоб зробити об’єкт range ітерабельним (і таким чином дозволити for..of працювати), нам потрібно додати метод до об’єкта з назвою Symbol.iterator (спеціальний вбудований символ саме для цього).

  1. Коли for..of запускається, він викликає цей метод один раз (або викидає помилку, якщо цей метод не знайдено). Метод повинен повернути iterator – об’єкт з методом next.
  2. Далі for..of працює лише з поверненим об’єктом.
  3. Коли for..of хоче отримати наступне значення, він викликає next() на цьому об’єкті.
  4. Результат next() повинен мати вигляд {done: Boolean, value: any}, де done=true означає, що ітерація завершена, інакше value – це наступне значення.

Ось повна реалізація об’єкту range із зауваженнями:

let range = {
  from: 1,
  to: 5
};

// 1. виклик for..of спочатку викликає цю функцію
range[Symbol.iterator] = function() {

  // 2. Далі, for..of працює тільки з цим ітератором, запитуючи у нього наступні значення
  return {
    next() {
      // 4. він повинен повертати значення як об’єкт {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// тепер це працює!
for (let num of range) {
  alert(num); // 1, потім 2, 3, 4, 5
}

Будь ласка, зверніть увагу на основну особливість ітеративних об’єктів: розділення проблем.

  • Сам range не має методу next().
  • Натомість інший об’єкт, так званий “ітератор”, створюється за допомогою виклику range[Symbol.iterator](), а його next() генерує значення для ітерації.

Отже, об’єкт, що ітерує відокремлений від об’єкта, який він ітерує.

Технічно, ми можемо об’єднати їх і використовувати range в якості ітератора, щоб зробити код простішим.

Подібно до цього:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  alert(num); // 1, тоді 2, 3, 4, 5
}

Тепер range[Symbol.iterator]() повертає сам об’єкт range: він має необхідний next() метод і пам’ятає поточну ітерацію прогресу в this.current. Коротше? Так. А іноді це також добре.

Недоліком є те, що тепер неможливо мати два for..of цикли паралельно для проходження через об’єкт: вони будуть ділити ітераційний стан, тому що є тільки один ітератор – сам об’єкт. Але два паралельних for-of це рідкісний випадок, навіть у асинхронізованих сценаріях.

Infinite iterators

Також можливі нескінченні ітератори. Наприклад, range стає нескінченним для range.to = Infinity. Або ми можемо зробити ітерований об’єкт, який генерує нескінченну послідовність псевдорандомних чисел. Це також може бути корисним.

Немає обмежень на next, він може повертати все більше і більше значень, це нормально.

Звичайно, for..of цикли через такий об’єкт буде нескінченним. Але ми завжди можемо зупинити його за допомогою break.

Рядок є ітерованим

Масиви та рядки найбільш широко використовуються вбудовані ітератори.

Для рядка, for..of цикл проходить по символам:

for (let char of "test") {
  // викликається 4 рази: один раз для кожного символу
  alert( char ); // t, потім e, потім s, потім t
}

І це правильно працює з сурогатними парами!

let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, і потім 😂
}

Виклик ітератора явно

Для глибшого розуміння, подивімось, як явно використовувати ітератор.

Ми будемо ітерувати рядок точно так само, як для for..of, але з прямими викликами. Цей код створює ітератор рядка і отримує значення від нього “вручну”:

let str = "Привіт";

// робить те ж саме, як
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // виводить символи один за одним
}

Це рідко потрібно, але дає нам більше контролю над процесом, ніж for ..of. Наприклад, ми можемо розділити процес ітерації: трохи ітерувати, а потім зупинитися, зробити щось інше, а потім відновити пізніше.

Ітеровані об’єкти та псевдомасиви

Ці два офіційних терміни виглядають подібними, але дуже різні. Будь ласка, переконайтеся, що ви добре розумієте їх, щоб уникнути плутанини.

  • Ітеровані – це об’єкти, які реалізують метод Symbol.iterator, як описано вище.
  • Псевдомасиви – це об’єкти, які мають індекси та length, тому вони виглядають як масиви.

Коли ми використовуємо JavaScript для практичних завдань у браузері або будь-якому іншому середовищі, ми можемо зустріти об’єкти, які є ітерованими або масивами, або обома. Наприклад, рядки є ітерованими об’єктами (for..of працює на них) та псевдомасивами (у них є числові індекси та length).

Але ітерований об’єкт може не бути масивом. І навпаки, псевдомасив може бути не ітерованим об’єктом.

Наприклад, range у прикладі вище є ітерованим об’єктом, але не масивом, тому що він не має індексованих властивостей та length.

І ось об’єкт, який є псевдомасивом, але не ітерованим об’єктом:

let arrayLike = { // має індекси та length => псевдомасив
  0: "Hello",
  1: "World",
  length: 2
};

// Помилка (немає Symbol.iterator)
for (let item of arrayLike) {}

Обидва, ітерований об’єкт та псевдомасив, як правило є не масивами, вони не мають push,pop та ін. Це досить незручно, якщо у нас є такий об’єкт і ми хочемо працювати з ним як з масивом. Наприклад, ми хотіли б працювати з angy за допомогою методів масиву. Як цього досягти?

Array.from

Існує універсальний метод Array.from, який приймає ітерований об’єкт або псевдомасив і робить з нього “справжній” масив. Тоді ми можемо викликати на ньому методи масиву.

Наприклад:

let arrayLike = {
  0: "Привіт",
  1: "Світ",
  length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // Світ (метод працює)

Array.from у рядку (*) бере об’єкт, перевіряє його на ітерабельність або те, що це псевдомасив, потім створює новий масив і копіює до нього всі елементи.

Те ж саме відбувається і з ітерованим об’єктом:

// припустимо, що діапазон взятий з наведеного вище прикладу
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion works)

Повний синтаксис для Array.from також дозволяє нам надати додаткову функцію “трансформації”:

Array.from(obj[, mapFn, thisArg])

The optional second argument mapFn can be a function that will be applied to each element before adding it to the array, and thisArg allows us to set this for it.

Наприклад:

// припустимо, що діапазон взятий з наведеного вище прикладу

// порахуємо квадрат кожного числа
let arr = Array.from(range, num => num * num);

alert(arr); // 1,4,9,16,25

Тут ми використовуємо Array.from, щоб перетворити рядок у масив символів:

let str = '𝒳😂';

// розіб’ємо рядок на масив символів
let chars = Array.from(str);

alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2

На відміну від str.split, він спирається на ітерабельний характер рядка і тому, так само, як for..of, коректно працює з сурогатними парами.

Технічно тут це відбувається так само, як:

let str = '𝒳😂';

let chars = []; // Array.from внутрішньо робить цей самий цикл
for (let char of str) {
  chars.push(char);
}

alert(chars);

function slice(str, start, end) { return Array.from(str).slice(start, end).join(’’); }

let str = ‘𝒳😂𩷶’;

alert( slice(str, 1, 3) ); // 😂𩷶

// нативний метод не підтримує сурогатні пари alert( str.slice(1, 3) ); // сміття (дві частини з різних сурогатних пар)

## Підсумки

Об’єкти, які можна використовуватися у `for..of`, називаються *ітерованими*.

- Технічно ітеровані об’єкти повинні реалізовувати метод з назвою `Symbol.iterator`.
    - Результат `obj[Symbol.iterator]()` називається *ітератором*. Він забезпечує подальший процес ітерації.
    - An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the end of the iteration process, otherwise the `value` is the next value.
    - Ітератор повинен мати метод з назвою `next()`, який повертає об’єкт `{done: Boolean, value: any}`, де `done: true` означає кінець процесу ітерації, інакше `value` є наступним значенням.
- Метод `Symbol.iterator` автоматично викликається `for..of`, але ми також можемо це зробити безпосередньо.
- Вбудовані ітеровані об’єкти, такі як рядки або масиви, також реалізують `Symbol.iterator`.
- Рядковий ітератор знає про сурогатні пари.


Об’єкти, які мають індексовані властивості та `length`, називаються *псевдомасивами*. Такі об’єкти також можуть мати інші властивості та методи, але не мають вбудованих методів масивів.

Якщо ми заглянемо в специфікацію -- ми побачимо, що більшість вбудованих методів припускають, що вони працюють з ітерованими об’єктами або псевдомасивами замість "реальних" масивів, тому що це більш абстрактно.

`Array.from(obj[, mapFn, thisArg])` створює справжній `Array` з ітерованого об’єкту або псевдомасиву `obj`, і тоді ми можемо використовувати на ньому методи масиву. Необов’язкові аргументи `mapFn` та` thisArg` дозволяють нам застосовувати функції до кожного елемента.
Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)