Итерируемые объекты в JavaScript

Итерируемые объекты и индексы в JavaScript

Технический фундамент итерации

До выбора конкретного цикла важно понимать механику языка: как значения становятся итерируемыми и по каким правилам движок проходит элементы. Чтобы не ловить ошибки времени выполнения, нужно понимать три вещи:

  • что в JS итерируемо;
  • где есть индекс, а где его нет;
  • какой цикл выбрать под задачу.

Ключевой момент: сначала определяют тип структуры и способ доступа к элементам, и только потом выбирают конкретный цикл.

Простейшая техническая проверка "итерируемо или нет":

const samples = [
  [1, 2], // Array
  'JS', // String
  new Set([1, 2]), // Set
  new Map([['a', 1]]), // Map
  { a: 1 }, // plain object
];

for (const value of samples) {
  console.log(typeof value[Symbol.iterator] === 'function');
}
// true, true, true, true, false

Индексы есть не у всех коллекций, даже если их можно итерировать:

const arr = ['A', 'B'];
const str = 'JS';
const set = new Set(['x', 'y']);
const map = new Map([['k1', 10], ['k2', 20]]);

console.log(arr[0]); // 'A' (индекс есть)
console.log(str[0]); // 'J' (позиция есть)
console.log(set[0]); // undefined (индекса нет)
console.log(map[0]); // undefined (доступ не по индексу)

Эти два примера дают базовое правило: "итерируемо" не означает "индексируемо".

Краткая памятка по типам и итерации:

Итерируемы из коробки:

  • Array
  • String
  • Set
  • Map
  • Typed arrays (Uint8Array, Int32Array и т.д.)
  • результаты Object.keys/values/entries (это массивы)

Не итерируемы из коробки:

  • Object (plain object {})
  • Number
  • Boolean
  • null
  • undefined

Важно: почти любой объект можно сделать итерируемым вручную, если добавить метод Symbol.iterator.

Iterable и iterator: базовая модель

Iterable - это значение, у которого есть метод Symbol.iterator.
Этот метод возвращает iterator - объект с методом next(), который по шагам отдает данные.

const arr = [10, 20];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Разные инструменты обхода (for, for...of, spread, Array.from) опираются на этот механизм по-разному.

Проверь себя: почему for...of не может работать там, где нет Symbol.iterator?

Что итерируемо из коробки

Чаще всего ты встретишь:

  • Array - итерируется по значениям;
  • String - по символам;
  • Set - по значениям;
  • Map - по парам [key, value];
  • результаты Object.keys(...), Object.values(...), Object.entries(...) - это массивы, значит они итерируемы.
for (const ch of 'JS') {
  console.log(ch); // J, S
}

Где есть индекс, а где его нет

Важно не путать понятия:

  • в массиве есть числовой индекс (arr[0], arr[1]);
  • в строке есть позиция символа (str[0]), но строка неизменяема;
  • в Set нет концепции индекса элемента;
  • в Map доступ идет по ключу, не по индексу;
  • в обычном объекте {} доступ идет по ключам свойств.

Проверь себя: почему вопрос "какой индекс у элемента в Set?" некорректен?

Как выбрать цикл: for, for...of, for...in

for используй, когда нужен явный индекс и контроль шага:

const scores = [50, 72, 90];
for (let i = 0; i < scores.length; i++) {
  console.log(i, scores[i]);
}

for...of используй, когда нужны значения и индекс не важен:

for (const score of scores) {
  console.log(score);
}

for...in проходит по ключам свойств объекта (и может захватывать унаследованные свойства), поэтому для массивов обычно не рекомендуется:

const user = { name: 'Anna', age: 20 };
for (const key in user) {
  console.log(key, user[key]);
}

Практичнее для объектов:

for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

Как получить и значение, и индекс в for...of

Если нужен и индекс, и элемент, используй entries():

const tags = ['js', 'ts', 'node'];

for (const [index, tag] of tags.entries()) {
  console.log(index, tag);
}

Это часто читается лучше, чем ручной счетчик, когда шаг всегда +1.

Array-like != iterable

Есть структуры, похожие на массив по виду (length, числовые ключи), но они не всегда итерируемы.
Поэтому "похоже на массив" не равно "можно в for...of".

Безопасный прием для нормализации:

const normalized = Array.from(input);

Смотри, что важно: Array.from(...) умеет работать и с iterable, и с array-like (у которых есть length и элементы по индексам). Но если объект не iterable и не array-like, можно получить «тихий» пустой массив, и это тоже источник багов.

console.log(Array.from({ length: 3, 0: 'a', 1: 'b', 2: 'c' })); // ['a', 'b', 'c']
console.log(Array.from({ a: 1 })); // []

Используй Array.from осознанно: когда ты понимаешь, какой именно формат входа ожидается.

Практика: безопасный перебор входных данных

function printItems(input) {
  if (input == null) return; // null/undefined

  if (typeof input[Symbol.iterator] !== 'function') {
    console.log('Not iterable');
    return;
  }

  for (const item of input) {
    console.log(item);
  }
}

Такой паттерн защищает от runtime-ошибок, если данные пришли извне.

Дополнительный пример для новичка: как выбрать способ обхода

function describeInput(input) {
  if (input == null) return 'no data';

  if (typeof input[Symbol.iterator] === 'function') {
    let count = 0;
    for (const _ of input) count++;
    return `iterable(${count})`;
  }

  if (typeof input === 'object') {
    return `object(${Object.keys(input).length})`;
  }

  return typeof input;
}

Этот пример показывает практический алгоритм: сначала проверка на null, затем проверка итерируемости, и только потом отдельная ветка для обычного объекта.

Частые ошибки новичков

  • Пытаться запускать for...of по обычному объекту {}.
  • Путать for...in (ключи) и for...of (значения).
  • Считать, что индекс есть у любой коллекции.
  • Писать i <= arr.length и получать лишнюю итерацию с undefined.
  • Не проверять входные данные на null/undefined перед циклом.

Анти-провал: перед циклом ответь на 3 вопроса: "что за структура?", "нужен ли индекс?", "какой тип результата нужен - ключ, значение или пара?".

Краткий итог

  • Итерируемость определяется протоколом Symbol.iterator.
  • for...of работает со значениями итерируемых структур.
  • Индексы характерны в первую очередь для массивов и строк, но не для Set/Map.
  • Для объектов чаще выбирают Object.keys/values/entries + for...of.
  • Правильный выбор цикла снижает количество ошибок и делает код предсказуемым.