Протоколи ітерування
Протоколи ітерування – це не нові вбудовані значення чи синтаксис, а протоколи. Ці протоколи можуть бути реалізовані будь-яким об'єктом, шляхом виконання певних умов.
Є два протоколи: Протокол ітерованого об'єкта та протокол ітератора.
Протокол ітерованого об'єкта
Протокол ітерованого об'єкта дає об'єктам JavaScript змогу означити або налаштувати власну логіку ітерування, наприклад, те, які значення обробляються в циклі for...of
. Частина вбудованих типів є вбудованими ітерованими об'єктами з усталеною логікою ітерування, як то Array
і Map
, коли інші (як то Object
) – ні.
Аби бути ітерованим, об'єкт мусить реалізувати метод [Symbol.iterator]()
, що означає, що об'єкт (або один з об'єктів у його ланцюжку прототипів) мусить мати властивість з ключем [Symbol.iterator]
, доступним через сталу Symbol.iterator
:
[Symbol.iterator]()
Функція без аргументів, що повертає об'єкт, котрий виконує протокол ітератора.
Щоразу, коли об'єкт готується до ітерування (наприклад, на початку циклу for...of
), викликається без аргументів його метод [Symbol.iterator]()
, і повернений ітератор використовується для отримання значень ітерування.
Зверніть увагу, що коли викликається ця функція без аргументів, вона закликається як метод ітерованого об'єкта. Таким чином, всередині цієї функції для звертання до властивостей ітерованого об'єкта може використовуватися ключове слово this
, аби з'ясувати, що повинно видаватися під час ітерації.
Ця функція може бути звичайною функцією, а може бути генераторною функцією, щоб при її заклику повертався об'єкт-ітератор. Всередині такої генераторної функції кожен запис надається за допомогою yield
.
Протокол ітератора
Протокол ітератора задає стандартний спосіб вироблення послідовності (або скінченної, або нескінченної) значень, і, потенційно, повернене значення, коли всі елементи послідовності були згенеровані.
Об'єкт є ітератором, коли має реалізацію метода next()
з наступною семантикою:
next()
Функція, що приймає нуль або один аргумент і повертає об'єкт, що відповідає інтерфейсові
IteratorResult
(див. нижче). Якщо повертається необ'єктне значення (наприклад,false
абоundefined
), коли ітератор використовується вбудованою можливістю мови (як тоfor...of
), то викидаєтьсяTypeError
("iterator.next() returned a non-object value"
).
Від усіх методів протоколу ітератора (next()
, return()
і throw()
) очікується повернення об'єкта, що реалізує інтерфейс IteratorResult
. Такий об'єкт повинен мати наступні властивості:
done
Необов'язковеБулеве значення, котре дорівнює
false
, якщо ітератор зміг виробити наступне значення послідовності. (Це рівносильно тому, щоб не задати властивістьdone
узагалі.)Має значення
true
, якщо ітератор завершив свою послідовність. У такому випадкуvalue
є необов'язковим завершальним значенням ітератора.value
Необов'язковеБудь-яке значення JavaScript, повернене ітератором. Може бути опущено, коли значення
done
дорівнюєtrue
.
На практиці жодна з цих властивостей не є суворо обов'язковою; якщо повертається об'єкт без якої-небудь властивості, то це фактично рівносильно поверненню { done: false, value: undefined }
.
Коли ітератор повертає результат із done: true
, то очікується, що всі наступні виклики next()
так само повернуть done: true
, хоч це і не вимагається на рівні мови.
Метод next
може прийняти значення, котре буде доступним тілу метода. Жодна вбудована можливість мови ніякого значення передавати не буде. Значення, передане в метод next
генераторів, стане значенням відповідного виразу yield
.
Крім цього, ітератор може, необов'язково, реалізувати методи return(value)
і throw(exception)
, котрі, бувши викликаними, кажуть ітераторові, що викликач закінчив ітерування і можна виконати яке-небудь важливе очищення (як то закриття з'єднання з базою даних).
return(value)
Необов'язковеФункція, котра приймає нуль або один аргумент і повертає об'єкт, котрий відповідає інтерфейсові
IteratorResult
, зазвичай із полемvalue
, рівним переданому в ньогоvalue
, і полемdone
зі значеннямtrue
. Виклик цього метода каже ітераторові, що викликач не має наміру більше викликатиnext()
і що можна зайнятися очищенням. Коли вбудовані можливості мови викликаютьreturn()
задля очищення, тоvalue
завждиundefined
.throw(exception)
Необов'язковеФункція, котра приймає нуль або один аргумент і повертає об'єкт, що відповідає інтерфейсові
IteratorResult
, зазвичай із полемdone
, рівнимtrue
. Виклик цього метода каже ітераторові, що викликач увійшов у помилковий стан, аexception
зазвичай є примірникомError
. Жодні вбудовані можливості мови не викликаютьthrow()
для потреб очищення: це спеціальна можливість для генераторів, створена заради симетричності міжreturn
іthrow
.
[!NOTE] Неможливо рефлективно дізнатися (тобто без фактичного виклику
next()
і перевірки поверненого результату), чи реалізує певний об'єкт протокол ітератора.
Дуже легко зробити ітератор також ітерованим об'єктом: достатньо реалізувати метод [Symbol.iterator]()
, котрий повертає this
.
// Відповідає і протоколові ітератора, і протоколові ітерованого об'єкта
const myIterator = {
next() {
// ...
},
[Symbol.iterator]() {
return this;
},
};
Такий об'єкт зветься ітерованим ітератором. Це дає змогу використати ітератор в певних синтаксичних структурах, що розраховують на ітеровані об'єкти – таким чином, нечасто є корисною реалізація протоколу ітератора без реалізації водночас протоколу ітерованого об'єкта. (Насправді майже всі мовні структури й API очікують на ітеровані об'єкти, а не ітератори.) Генераторний об'єкт є прикладом цього:
const aGeneratorObject = (function* () {
yield 1;
yield 2;
yield 3;
})();
console.log(typeof aGeneratorObject.next);
// "function": є метод next (котрий повертає правильний результат), тож це ітератор
console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function": є метод [Symbol.iterator] (котрий повертає правильний ітератор), тож це ітерований об'єкт
console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true: метод [Symbol.iterator] повертає сам об'єкт (сам ітератор), тож це ітерований ітератор
Усі вбудовані ітератори мають в ланцюжку прототипів об'єкт Iterator.prototype
, котрий має реалізацію методу [Symbol.iterator]()
, що повертає this
, тож вбудовані ітератори також є ітерованими об'єктами.
А проте, коли це можливо, краще, щоб iterable[Symbol.iterator]()
повертав різні ітератори, що завжди починаються спочатку, як це робить Set.prototype[Symbol.iterator]()
.
Асинхронний ітератор і протокол асинхронного ітерованого об'єкта
Є іще одна пара протоколів, яка використовується для асинхронного ітерування – вони звуться протоколами асинхронного ітератора й асинхронного ітерованого об'єкта. Їх інтерфейси дуже схожі на інтерфейси протоколів ітерованого об'єкта та ітератора, окрім того, що кожне повернене при викликах методів ітератора значення – загорнуте в проміс.
Об'єкт реалізує протокол асинхронного ітерованого об'єкта, коли має реалізацію наступних методів:
[Symbol.asyncIterator]()
Функція без аргументів, що повертає об'єкт, котрий відповідає протоколові асинхронного ітератора.
Об'єкт реалізує протокол асинхронного ітератора, коли має реалізацію наступних методів:
next()
Функція, котра приймає нуль або один аргумент і повертає проміс. Цей проміс сповнюється об'єктом, що відповідає інтерфейсові
IteratorResult
, а його властивості мають таку ж семантику, як в синхронного ітератора.return(value)
Необов'язковеФункція, котра приймає нуль або один аргумент і повертає проміс. Цей проміс сповнюється об'єктом, що відповідає інтерфейсові
IteratorResult
, а його властивості мають таку ж семантику, як в синхронного ітератора.throw(exception)
Необов'язковеФункція, котра приймає нуль або один аргумент і повертає проміс. Цей проміс сповнюється об'єктом, що відповідає інтерфейсові
IteratorResult
, а його властивості мають таку ж семантику, як в синхронного ітератора.
Взаємодія між мовою та протоколами ітерування
Мова задає API, котрі або виробляють, або приймають ітеровані об'єкти й ітератори.
Вбудовані ітеровані об'єкти
String
, Array
, TypedArray
, Map
, Set
і Segments
(повернений з Intl.Segmenter.prototype.segment()
) є вбудованими ітерованими об'єктами, адже кожний з їхніх об'єктів prototype
має реалізацію метода [Symbol.iterator]()
. На додачу, об'єкт arguments
і частина типів колекцій DOM, як то NodeList
, так само є ітерованими об'єктами.
У ядрі мови JavaScript немає жодного об'єкта, що був би асинхронним ітерованим. Деякі API Вебу, як то ReadableStream
, усталено мають метод Symbol.asyncIterator
.
Генераторні функції повертають генераторні об'єкти, котрі є ітерованими ітераторами. Асинхронні генераторні функції повертають асинхронні генераторні об'єкти, котрі є асинхронними ітерованими ітераторами.
Ітератори, повернені зі вбудованих ітерованих об'єктів, успадковують від спільного класу Iterator
(наразі прихованого), котрий має реалізацію вищезгаданого методу [Symbol.iterator]() { return this; }
, що робить їх всіх ітерованими ітераторами. У майбутньому ці вбудовані ітератори можуть отримати додаткові допоміжні методи, на додачу до методу next()
, котрий вимагається протоколом ітератора. Ланцюжок прототипів ітератора можна дослідити шляхом виведення його в графічну консоль.
console.log([][Symbol.iterator]());
Array Iterator {}
[[Prototype]]: Array Iterator ==> Цей прототип — спільний для всіх ітераторів-масивів
next: ƒ next()
Symbol(Symbol.toStringTag): "Array Iterator"
[[Prototype]]: Object ==> Цей прототип — спільний для всіх вбудованих ітераторів
Symbol(Symbol.iterator): ƒ [Symbol.iterator]()
[[Prototype]]: Object ==> Це — Object.prototype
Вбудовані API, що приймають ітератори
Є чимало API, що приймають ітеровані об'єкти. Серед прикладів:
Map()
WeakMap()
Set()
WeakSet()
Promise.all()
Promise.allSettled()
Promise.race()
Promise.any()
Array.from()
Object.groupBy()
Map.groupBy()
const myObj = {};
new WeakSet(
(function* () {
yield {};
yield myObj;
yield {};
})(),
).has(myObj); // true
Синтаксичні конструкції, що очікують на ітеровані об'єкти
Частина інструкцій і виразів очікує на ітеровані об'єкти, наприклад, цикли for...of
, розгортання масивів і параметрів, yield*
і деструктурування масивів:
for (const value of ["a", "b", "c"]) {
console.log(value);
}
// "a"
// "b"
// "c"
console.log([..."abc"]); // ["a", "b", "c"]
function* gen() {
yield* ["a", "b", "c"];
}
console.log(gen().next()); // { value: "a", done: false }
[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"
Коли вбудовані синтаксичні конструкції ітерують ітератор, і поле done
останнього результату дорівнює false
(тобто ітератор може виробити більше значень), але більше значень не потрібно, то буде викликано метод return
, якщо такий метод є. Це може статися, наприклад, якщо в циклі for...of
зустрілася інструкція break
або return
, або коли при деструктуруванні масиву усі ідентифікатори вже отримали значення.
const obj = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
i++;
console.log("Повернення", i);
if (i === 3) return { done: true, value: i };
return { done: false, value: i };
},
return() {
console.log("Закривання");
return { done: true };
},
};
},
};
const [a] = obj;
// Повернення 1
// Закривання
const [b, c, d] = obj;
// Повернення 1
// Повернення 2
// Повернення 3
// Уже досягнутий кінець (останній виклик повернув `done: true`),
// тож `return` не викликається
for (const b of obj) {
break;
}
// Повернення 1
// Закривання
Цикл for await...of
і yield*
в асинхронних генераторних функціях (але не синхронних генераторних функціях) – єдині способи взаємодіяти з асинхронними ітерованими об'єктами. Використання for...of
, розгортання масиву тощо на асинхронному ітерованому об'єкті, що не є водночас синхронним ітерованим об'єктом (тобто має [Symbol.asyncIterator]()
, але не має [Symbol.iterator]()
) викине TypeError: x is not iterable.
Обробка помилок
Оскільки ітерація включає передачу контролю назад і вперед між ітератором і його споживачем, обробка помилок відбувається в обидва боки: те, як споживач обробляє помилки, викинуті ітератором, і те, як ітератор обробляє помилки, викинуті споживачем. Коли використовується один зі вбудованих способів ітерування, мова також може викидати помилки, бо ітерований об'єкт порушує певні інваріанти. Ми опишемо, як вбудовані записи синтаксису генерують і обробляють помилки, і цей текст може вживатися як настанова для вашого власного коду, якщо ви вручну обходите ітератор.
Погано сформовані ітеровані об'єкти
Помилки можуть відбуватися під час отримання ітератора з ітерованого об'єкта. Мовний інваріант, накладений тут, – це те, що ітерований об'єкт повинен видавати валідний ітератор:
- Він має викличний метод
[Symbol.iterator]()
. - Метод
[Symbol.iterator]()
повертає об'єкт. - Об'єкт, повернений
[Symbol.iterator]()
, має викличний методnext()
.
Коли для ініціації ітерування на погано сформованому ітерованому об'єкті вживається вбудований синтаксис, викидається TypeError.
const nonWellFormedIterable = { [Symbol.iterator]: 1 };
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable is not iterable
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [Symbol.iterator]() returned a non-object value
nonWellFormedIterable[Symbol.iterator] = () => ({});
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable[Symbol.iterator]().next is not a function
Якщо в асинхронних ітерованих об'єктах властивість [Symbol.asyncIterator]()
має значення undefined
або null
, то JavaScript відступає до вживання натомість властивості [Symbol.iterator]
(і загортає результівний ітератор в асинхронний ітератор, переспрямовуючи методи). Інакше властивість [Symbol.asyncIterator]
також повинна відповідати інваріантам вище.
Таким помилкам можна запобігти, валідуючи ітерований об'єкт перед спробою його ітерування. Проте це трапляється доволі рідко, адже зазвичай тип об'єкта, що ітерується, відомий. Якщо ви отримуєте ітерований об'єкт з якогось іншого коду, то вам слід просто дати помилці поширитися до викликача, щоб він знав: було надано невалідні вихідні дані.
Помилки під час ітерування
Більшість помилок трапляються під час обходу ітератора (викликаючи next()
). Мовний інваріант, що тут накладається, полягає в тому, що метод next()
повинен повертати об'єкт (у разі асинхронних ітераторів – об'єкт після очікування). Інакше викидається TypeError.
Якщо інваріант порушено або метод next()
викидає помилку (для асинхронних ітераторів також може бути повернення відхиленого промісу), то помилка поширюється до викликача. У вбудованих записах синтаксису така ітерація переривається без повторних спроб і очищення (виходячи з припущення, що якщо метод next()
викинув помилку, то все уже очищено). Якщо ви вручну викликаєте next()
, то можете перехопити помилку і спробувати повторно викликати next()
, проте загалом слід припускати, що ітератор вже закрито.
Якщо викликач вирішить вийти з ітерування з якихось причин, відмінних від помилок, описаних в абзаці вище, наприклад, коли він перейшов до стану помилки у власному коді (наприклад, у разі обробки невалідного значення, виданого ітератором), йому слід викликати метод return()
на ітераторі, якщо такий є. Це дасть ітератору змогу виконати якесь очищення. Метод return()
викликається лише для раннього виходу: якщо next()
повертає done: true
, то метод return()
не викликається, виходячи з припущення, що ітератор уже очистився.
Метод return()
так само може бути невалідним! Мова також вимагає, щоб метод return()
повертав об'єкт, а інакше викидав TypeError. Якщо метод return()
викидає помилку, то ця помилка поширюється до викликача. Проте якщо метод return()
викликається, тому що викликач зіткнувся з помилкою у своєму власному коді, то така помилка замінює помилку, викинуту методом return()
.
Зазвичай викликач реалізує обробку помилок ось так:
try {
for (const value of iterable) {
// ...
}
} catch (e) {
// Обробити помилку
}
catch
зможе перехопити помилки, викинуті, коли iterable
не є валідним ітерованим об'єктом, коли next()
викидає помилку, коли return()
викидає помилку (якщо цикл for
завершується передчасно) або коли тіло циклу for
викидає помилку.
Більшість ітераторів реалізовані за допомогою функцій-генераторів, тож ми продемонструємо, як функції-генератори зазвичай обробляють помилки:
function* gen() {
try {
yield doSomething();
yield doSomethingElse();
} finally {
cleanup();
}
}
Відсутність catch
змушує помилки, викинуті doSomething()
або doSomethingElse()
, поширюватися до викликача gen
. Якщо ці помилки перехоплюються всередині функції-генератора (що так само вітається), то така функція-генератор може вирішити продовжити видавати значення або завершитися передчасно. Проте блок finally
необхідний для генераторів, що зберігають ресурси відкритими. Блок finally
гарантовано запуститься, коли або востаннє викличеться next()
, або викличеться return()
.
Переспрямування помилок
Частина вбудованих записів синтаксису загортає ітератор в інший ітератор. Серед них ітератор, що створюється методом Iterator.from()
, ітераторні помічники (map()
, filter()
, take()
, drop()
і flatMap()
), yield*
і прихована обгортка, коли щодо синхронного ітератора використовується асинхронна ітерація (for await...of
, Array.fromAsync
). Загорнутий ітератор тоді відповідає за переспрямування помилок між внутрішнім ітератором і викликачем.
- Усі ітератори-обгортки безпосередньо переспрямовують метод
next()
внутрішнього ітератора, включно з поверненими значеннями та викинутими помилками. - Ітератори-обгортки загалом безпосередньо переспрямовують метод
return()
внутрішнього ітератора. Якщо методreturn()
на внутрішньому ітераторі не існує, то зовнішній повертає натомість{ done: true, value: undefined }
. У разі ітераторних помічників: якщо методnext()
ітераторного помічника ще не був викликаний, то після спроби викликатиreturn()
на внутрішньому ітераторі поточний ітератор завжди повертає{ done: true, value: undefined }
. Ця поведінка узгоджена з функціями-генераторами, в яких плин виконання ще не дійшов до виразуyield*
. yield*
– це єдиний вбудований запис синтаксису, що переспрямовує методthrow()
внутрішнього ітератора. Про те, якyield*
переспрямовує методиreturn()
іthrow()
, читайте в його власній довідці.
Приклади
Користувацькі ітеровані об'єкти
Так можна створювати власні ітеровані об'єкти:
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
console.log([...myIterable]); // [1, 2, 3]
Простий ітератор
Ітератори за своєю природою мають стан. Якщо не означити ітератор як генераторну функцію (як це показано в прикладі вище), то, ймовірно, доведеться інкапсулювати цей стан в замиканні.
function makeIterator(array) {
let nextIndex = 0;
return {
next() {
return nextIndex < array.length
? {
value: array[nextIndex++],
done: false,
}
: {
done: true,
};
},
};
}
const it = makeIterator(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true
Нескінченний ітератор
function idMaker() {
let index = 0;
return {
next() {
return {
value: index++,
done: false,
};
},
};
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...
Означення ітерованого об'єкта за допомогою генератора
function* makeSimpleGenerator(array) {
let nextIndex = 0;
while (nextIndex < array.length) {
yield array[nextIndex++];
}
}
const gen = makeSimpleGenerator(["yo", "ya"]);
console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done); // true
function* idMaker() {
let index = 0;
while (true) {
yield index++;
}
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...
Означення ітерованого об'єкта за допомогою класу
Інкапсуляція стану можлива також за допомогою приватних властивостей.
class SimpleClass {
#data;
constructor(data) {
this.#data = data;
}
[Symbol.iterator]() {
// Використання нового індексу для кожного ітератора. Це робить кілька
// ітерувань одного ітерованого безпечними для нетривіальних випадків,
// як то використання break або вкладеного ітерування одного ітерованого.
let index = 0;
return {
// Примітка: використання стрілкової функції дає `this` змогу вказувати на
// `this` методу `[Symbol.iterator]()`, а не `this` методу `next()`
next: () => {
if (index < this.#data.length) {
return { value: this.#data[index++], done: false };
} else {
return { done: true };
}
},
};
}
}
const simple = new SimpleClass([1, 2, 3, 4, 5]);
for (const val of simple) {
console.log(val); // 1 2 3 4 5
}
Заміщення вбудованих ітерованих об'єктів
Наприклад, String
– це вбудований ітерований об'єкт:
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"
Усталений ітератор String
повертає одну за одною кодові точки рядка:
const iterator = someString[Symbol.iterator]();
console.log(`${iterator}`); // "[object String Iterator]"
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Можна перевизначити логіку ітерування, задавши власний [Symbol.iterator]()
::
// необхідно сконструювати об'єкт String явно, аби уникнути автоматичного пакування
const someString = new String("hi");
someString[Symbol.iterator] = function () {
return {
// це об'єкт-ітератор, котрий повертає один-єдиний елемент(рядок "bye")
next() {
return this._first
? { value: "bye", done: (this._first = false) }
: { done: true };
},
_first: true,
};
};
Зверніть увагу, як заміщення [Symbol.iterator]()
впливає на поведінку вбудованих конструкцій, котрі користуються протоколом ітерування:
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"
Рівночасне внесення змін під час ітерування
Майже всі ітеровані об'єкти мають однакову внутрішню семантику: вони не копіюють дані, коли починається ітерування. Замість цього вони зберігають вказівник і змінюють його значення. Так, якщо додати, видалити або змінити елементи під час ітерування колекції, можна ненароком змінити те, чи будуть оброблені інші, тобто ще не змінені, елементи колекції. Це дуже схоже на те, як працюють ітерувальні методи масивів.
Для прикладу – наступний зразок з використанням URLSearchParams
:
const searchParams = new URLSearchParams(
"deleteme1=value1&key2=value2&key3=value3",
);
// Видалити небажані ключі
for (const [key, value] of searchParams) {
console.log(key);
if (key.startsWith("deleteme")) {
searchParams.delete(key);
}
}
// Вивід:
// deleteme1
// key3
Зверніть увагу на те, що key2
взагалі не виводиться. Це пов'язано з тим, що URLSearchParams
усередині є списком пар ключа та значення. Коли deleteme1
обробляється та видаляється, всі інші записи зсуваються на один уліво, тож key2
займає те місце, в якому раніше був deleteme1
, і коли вказівник переходить до наступного ключа, то опиняється на key3
.
Певні реалізації ітерованих уникають цієї проблеми, задаючи "надгробки" значень, аби не зсувати решту значень. Для прикладу – подібний код з використанням Map
:
const myMap = new Map([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myMap) {
console.log(key);
if (key.startsWith("deleteme")) {
myMap.delete(key);
}
}
// Вивід:
// deleteme1
// key2
// key3
Зверніть увагу на те, що виводяться всі ключі. Це пов'язано з тим, що Map
не зсуває решту ключів, коли один з них видалено. Якщо хочете реалізувати щось подібне, то ось який вигляд це може мати:
const tombstone = Symbol("tombstone");
class MyIterable {
#data;
constructor(data) {
this.#data = data;
}
delete(deletedKey) {
for (let i = 0; i < this.#data.length; i++) {
if (this.#data[i][0] === deletedKey) {
this.#data[i] = tombstone;
return true;
}
}
return false;
}
*[Symbol.iterator]() {
for (let i = 0; i < this.#data.length; i++) {
if (this.#data[i] !== tombstone) {
yield this.#data[i];
}
}
}
}
const myIterable = new MyIterable([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myIterable) {
console.log(key);
if (key.startsWith("deleteme")) {
myIterable.delete(key);
}
}
[!WARNING] Рівночасні зміни загалом дуже часто призводять до вад і плутанини. Якщо не знаєте з кришталевою ясністю, як працює ітерований об'єкт, краще уникайте внесення змін до колекції, над якою відбувається ітерація.