Пошук уроків, статей та іншого контенту
Розберете ітератори, entries, keys і values, а також особливості ітерації масивів у JavaScript.
Ітерація — це послідовне отримання значень із певного об’єкта. У JavaScript ітерація ґрунтується на двох протоколах:
протокол ітерованого об’єкта — визначає, як отримати ітератор;
протокол ітератора — визначає, як отримувати наступні значення.
Об’єкт є ітерованим (iterable), якщо має метод із ключем Symbol.iterator.
Ітератор — це об’єкт із методом next(). Кожен виклик next() повертає об’єкт такого формату:
{
value: значення,
done: false
}Після завершення перебору:
{
value: undefined,
done: true
}Наприклад:
const numbers = [10, 20, 30];
const iterator = numbers[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());Результат:
{ value: 10, done: false }
{ value: 20, done: false }
{ value: 30, done: false }
{ value: undefined, done: true }Масив є ітерованим об’єктом, а його ітератор повертає елементи за індексами.
Щоб об’єкт можна було використовувати у for...of, операторі розпилення або деструктуризації, він повинен реалізувати Symbol.iterator.
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
return {
next: () => {
if (current <= this.end) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
}
};
for (const number of range) {
console.log(number);
}Результат:
1
2
3
4
5У цьому прикладі:
range є ітерованим об’єктом.
Виклик range[Symbol.iterator]() створює ітератор.
Ітератор має метод next().
for...of викликає next() доти, доки done не стане true.
Зазвичай ітератор також реалізує власний [Symbol.iterator]() і повертає самого себе:
const iterator = {
current: 1,
next() {
if (this.current <= 3) {
return {
value: this.current++,
done: false
};
}
return {
value: undefined,
done: true
};
},
[Symbol.iterator]() {
return this;
}
};
for (const value of iterator) {
console.log(value);
}Це дозволяє використовувати ітератор безпосередньо у for...of.
Важлива особливість: такий ітератор є одноразовим. Після завершення перебору повторний for...of більше не поверне значень, оскільки внутрішній стан уже змінився.
console.log([...iterator]); // []На відміну від цього, масив можна перебирати багато разів, оскільки кожен виклик array[Symbol.iterator]() створює новий ітератор.
Масиви реалізують протокол перебору:
const values = [10, 20, 30];
console.log(typeof values[Symbol.iterator]);
console.log(values[Symbol.iterator] === values.values);Результат:
function
trueМетод values() є стандартним ітератором значень масиву, а [Symbol.iterator] масиву посилається на той самий механізм.
const values = [10, 20, 30];
for (const value of values) {
console.log(value);
}Цей код фактично використовує values():
const iterator = values.values();
for (const value of iterator) {
console.log(value);
}Оператор розпилення також використовує протокол ітерації:
const source = [10, 20, 30];
const copy = [...source];
console.log(copy);Деструктуризація масиву теж працює через ітератор:
const [first, second] = [10, 20, 30];
console.log(first); // 10
console.log(second); // 20values(), keys() і entries()Масиви мають три основні методи для отримання ітераторів.
values()values() повертає значення елементів:
const users = ["Anna", "Bohdan", "Olena"];
const iterator = users.values();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());Результат:
{ value: 'Anna', done: false }
{ value: 'Bohdan', done: false }
{ value: 'Olena', done: false }Найчастіше values() використовується неявно через for...of:
for (const user of users.values()) {
console.log(user);
}keys()keys() повертає індекси масиву:
const colors = ["red", "green", "blue"];
for (const index of colors.keys()) {
console.log(index);
}Результат:
0
1
2Це відрізняється від Object.keys():
array.keys() повертає ітератор;
Object.keys(array) повертає новий масив рядків.
const array = ["a", "b"];
console.log([...array.keys()]);
console.log(Object.keys(array));Результат:
[0, 1]
["0", "1"]entries()entries() повертає пари [індекс, значення]:
const products = ["Keyboard", "Mouse", "Monitor"];
for (const [index, product] of products.entries()) {
console.log(`${index}: ${product}`);
}Результат:
0: Keyboard
1: Mouse
2: MonitorЦе зручна альтернатива ручному доступу до індексу:
for (const [index, value] of array.entries()) {
// робота з index і value
}Порівняно з forEach, entries() добре підходить, коли потрібно:
використовувати break або continue;
застосовувати await у тілі циклу;
отримувати індекс і значення в одному кроці;
працювати із загальним протоколом ітерації.
Масив може містити пропущені позиції, які називають дірками (holes):
const sparse = [];
sparse[2] = "value";
console.log(sparse.length); // 3
console.log(sparse);Такий масив має довжину 3, але властивість із ключем "0" та "1" відсутня.
Ітератори масиву не пропускають ці позиції. Вони повертають undefined:
const sparse = [];
sparse[2] = "value";
console.log([...sparse.values()]);
console.log([...sparse.keys()]);
console.log([...sparse.entries()]);Результат:
[undefined, undefined, "value"]
[0, 1, 2]
[
[0, undefined],
[1, undefined],
[2, "value"]
]Це відрізняється від деяких методів масиву, наприклад forEach, map і filter, які зазвичай не викликають callback для відсутніх елементів:
const sparse = [];
sparse[2] = "value";
sparse.forEach((value, index) => {
console.log(index, value);
});Результат:
2 valueОтже, for...of та values() працюють із позиціями масиву, тоді як багато callback-методів працюють лише з наявними властивостями.
for...of і for...infor...of перебирає значення ітерованого об’єкта:
const numbers = [10, 20, 30];
for (const number of numbers) {
console.log(number);
}for...in перебирає імена enumerable-властивостей:
const numbers = [10, 20, 30];
for (const key in numbers) {
console.log(key);
}Результатом for...in будуть рядки:
0
1
2Основні відмінності:
for...of працює через Symbol.iterator;
for...in перебирає ключі властивостей;
for...of повертає значення;
for...in може включати власні та успадковані enumerable-властивості;
for...in не призначений для звичайного перебору масивів.
Додавання власної властивості до масиву добре демонструє різницю:
const values = [10, 20, 30];
values.label = "numbers";
for (const value of values) {
console.log(value);
}
for (const key in values) {
console.log(key);
}for...of поверне лише 10, 20, 30, а for...in також побачить властивість label.
Ітератор масиву зберігає поточний індекс і під час кожного виклику next() перевіряє поточний стан масиву. Через це зміни масиву під час перебору можуть впливати на результат.
const values = [1, 2, 3];
for (const value of values) {
console.log(value);
if (value === 3) {
values.push(4);
}
}У цьому випадку буде виведено і 4, оскільки масив змінився до завершення перебору.
const values = [1, 2, 3, 4];
for (const value of values) {
console.log(value);
if (value === 1) {
values.shift();
}
}Після shift() індекси зміщуються, тому частина елементів може бути пропущена.
Для передбачуваної поведінки краще:
не змінювати масив під час перебору;
перебирати копію;
спочатку зібрати зміни, а потім застосувати їх.
const values = [1, 2, 3, 4];
for (const value of [...values]) {
if (value % 2 === 0) {
console.log(value);
}
}return()Ітератор може мати метод return(). Він викликається, коли перебір завершується достроково, наприклад через break, return або помилку.
Це дає змогу звільнити ресурси: закрити з’єднання, завершити транзакцію або скасувати допоміжний процес.
const resource = {
[Symbol.iterator]() {
let current = 1;
return {
next() {
if (current <= 5) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
},
return() {
console.log("Ресурс звільнено");
return {
value: undefined,
done: true
};
}
};
}
};
for (const value of resource) {
console.log(value);
if (value === 2) {
break;
}
}Результат:
1
2
Ресурс звільненоЯкщо ітератор не має return(), дострокове завершення все одно відбудеться, але додаткової логіки очищення не буде.
Метод return() повинен повертати об’єкт-результат ітерації. Для цього зазвичай використовують:
return {
value: undefined,
done: true
};Ітерований об’єкт і ітератор — не обов’язково одне й те саме.
Масив — це ітерований об’єкт:
const array = [1, 2, 3];
const firstIterator = array[Symbol.iterator]();
const secondIterator = array[Symbol.iterator]();
console.log(firstIterator === secondIterator); // falseКожен ітератор має власний стан:
const array = [1, 2, 3];
const first = array.values();
const second = array.values();
console.log(first.next().value); // 1
console.log(first.next().value); // 2
console.log(second.next().value); // 1Ітератори незалежні один від одного.
Натомість генератор є ітерованим ітератором, тому його стан спільний для всіх способів читання:
function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
const generator = generateNumbers();
console.log(generator.next().value); // 1
console.log([...generator]); // [2, 3]
console.log([...generator]); // []Після споживання генератора його не можна почати спочатку.
Методи keys(), values() та entries() можуть працювати не лише з масивами. Їх можна викликати через call для масивоподібних об’єктів, які мають числову властивість length.
const arrayLike = {
0: "first",
1: "second",
length: 2
};
const values = Array.prototype.values.call(arrayLike);
console.log([...values]);Результат:
["first", "second"]Такий об’єкт не стає повноцінним масивом, але може бути джерелом для ітератора.
Це корисно для роботи з:
arguments;
колекціями DOM;
власними масивоподібними структурами;
об’єктами з властивостями 0, 1, 2 і length.
entries, keys і valuesМожна створити об’єкт, який підтримує кілька способів перебору:
class Collection {
constructor(items) {
this.items = [...items];
}
*values() {
for (const item of this.items) {
yield item;
}
}
*keys() {
for (let index = 0; index < this.items.length; index++) {
yield index;
}
}
*entries() {
for (let index = 0; index < this.items.length; index++) {
yield [index, this.items[index]];
}
}
[Symbol.iterator]() {
return this.values();
}
}
const collection = new Collection(["A", "B", "C"]);
console.log([...collection]);
console.log([...collection.keys()]);
console.log([...collection.entries()]);Результат:
["A", "B", "C"]
[0, 1, 2]
[
[0, "A"],
[1, "B"],
[2, "C"]
]Метод [Symbol.iterator]() визначає основний спосіб перебору об’єкта. У цьому прикладі основним є перебір значень.
Генератори зручні для реалізації ітераторів, оскільки JavaScript автоматично створює об’єкти результатів { value, done }.
Синхронний протокол використовує Symbol.iterator і метод next(), який повертає звичайний об’єкт.
Асинхронний протокол використовує:
Symbol.asyncIterator;
next(), який повертає Promise;
for await...of.
Ці протоколи не слід змішувати:
for (const value of synchronousIterable) {
console.log(value);
}і:
for await (const value of asynchronousIterable) {
console.log(value);
}Звичайний масив підтримує синхронний перебір. Його можна використовувати у for await...of, але це не робить сам масив асинхронним ітератором: значення просто обробляються як уже доступні.
next() у масивуМасив є ітерованим об’єктом, але не є ітератором:
const values = [1, 2, 3];
// values.next(); // TypeErrorПотрібно спочатку отримати ітератор:
const iterator = values.values();
console.log(iterator.next());keys() поверне значенняconst values = ["a", "b"];
console.log([...values.keys()]); // [0, 1]keys() повертає індекси, а не елементи. Для значень потрібно використовувати values() або звичайний for...of.
for...in для масивуfor...in перебирає ключі властивостей і повертає їх як рядки. Він може побачити додаткові enumerable-властивості та успадковані ключі.
Для значень масиву використовуйте for...of, а для індексів і значень — entries().
const iterator = [1, 2, 3].values();
console.log([...iterator]); // [1, 2, 3]
console.log([...iterator]); // []Ітератор споживається. Якщо потрібен повторний перебір, створюйте новий ітератор або зберігайте ітерований об’єкт, наприклад масив.
for...of повертає undefined для пропущених позицій:
const values = new Array(3);
for (const value of values) {
console.log(value); // undefined
}Не слід автоматично вважати, що відсутня позиція буде пропущена.
Додавання, видалення або переміщення елементів під час ітерації може змінити порядок і кількість оброблених значень. Якщо така поведінка не є навмисною, працюйте з копією або змінюйте масив окремою операцією.
Ітерований об’єкт має метод Symbol.iterator.
Ітератор має метод next(), який повертає { value, done }.
Масиви є ітерованими об’єктами, але сам масив не є ітератором.
values() перебирає значення, keys() — індекси, entries() — пари [індекс, значення].
for...of, оператор розпилення та деструктуризація використовують протокол ітерації.
for...in перебирає ключі властивостей і не є еквівалентом for...of.
Ітератори зазвичай одноразові, тоді як масив можна перебирати багато разів.
Ітератори масивів повертають undefined для дірок у розріджених масивах.
Зміна масиву під час ітерації може впливати на результат.
Метод return() ітератора використовується для коректного завершення та звільнення ресурсів.
Власні ітеровані об’єкти можна створювати вручну або за допомогою генераторів.