JS Symbol и итераторы

Symbol — уникальный примитивный тип данных, используемый как неповторяемый ключ свойства; итератор — объект с методом next, реализующий протокол последовательного обхода коллекции.

Зачем нужно

Symbol решает проблему коллизий имён свойств при расширении объектов из чужих библиотек — каждый Symbol уникален, даже если у них одинаковое описание. Итераторы — основа цикла for...of, деструктуризации и spread-оператора: именно они определяют, как объект «разворачивается». Понимание протокола итерирования необходимо для работы с генераторами, Map, Set, кастомными структурами данных.

Где используется

  • Уникальные ключи в объектах: избежать случайной перезаписи свойств при миксинах
  • Symbol.iterator — сделать объект итерируемым для for...of
  • Symbol.toPrimitive, Symbol.hasInstance, Symbol.toStringTag — переопределить встроенное поведение
  • Метки для приватных или «полупрохожих» свойств (не видны в for...in, не перечисляются JSON.stringify)
  • Кастомные итерируемые структуры данных: связные списки, деревья, бесконечные последовательности

Основной контент

Symbol

// Каждый Symbol уникален
const s1 = Symbol('id');
const s2 = Symbol('id');
console.log(s1 === s2); // false

// Использование как ключ объекта
const ID = Symbol('id');
const user = {
  name: 'Иван',
  [ID]: 42
};
console.log(user[ID]);      // 42
console.log(user['id']);    // undefined

// Не попадает в for...in и Object.keys()
for (const key in user) console.log(key); // только "name"
console.log(Object.keys(user));           // ["name"]
// Но виден через:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]

Встроенные well-known Symbols

// Symbol.toPrimitive — управление приведением типов
const money = {
  amount: 100,
  currency: 'RUB',
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.amount;
    if (hint === 'string') return `${this.amount} ${this.currency}`;
    return this.amount;
  }
};
console.log(+money);       // 100
console.log(`${money}`);   // "100 RUB"

// Symbol.toStringTag — переопределить вывод Object.prototype.toString()
class MyCollection {
  get [Symbol.toStringTag] { return 'MyCollection'; }
}
console.log(Object.prototype.toString().call(new MyCollection)); // [object MyCollection]

Протокол итерирования

// Итерируемый объект должен иметь метод Symbol.iterator,
// возвращающий итератор с методом next

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator] {
    let current = this.from;
    const last = this.to;
    return {
      next {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// Spread и деструктуризация работают благодаря Symbol.iterator
console.log([...range]); // [1, 2, 3, 4, 5]
const [a, b, c] = range;
console.log(a, b, c);    // 1 2 3

Кастомная итерируемая структура

class LinkedList {
  constructor {
    this.head = null;
  }

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  [Symbol.iterator] {
    let node = this.head;
    return {
      next {
        if (node) {
          const value = node.value;
          node = node.next();
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]); // [1, 2, 3]

Частые ошибки

  • Symbol не конвертируется неявно в строку: 'id: ' + Symbol('id') выбросит TypeError — нужно явно вызвать .toString() или .description.
  • Symbol.for vs Symbol: Symbol.for('key') возвращает глобальный разделяемый символ — два вызова с одним именем вернут один и тот же объект. Symbol всегда создаёт новый.
  • Итератор без возврата done: true приведёт к бесконечному циклу в for...of.
  • Забыть вернуть this из [Symbol.iterator]: итератор и итерируемый объект могут быть одним объектом, но next должен быть в возвращаемом объекте.

Связанные темы

Ресурсы


🎓 Источник: Proxy и Symbol в JavaScript

  • 📅 2018-11-26 · YouTube
  • Тезисы: Symbol — namespace для системных протоколов. Symbol.iterator, Symbol.asyncIterator, Symbol.toPrimitive, Symbol.hasInstance — это hooks, через которые runtime общается с объектами

🎓 Источник: Итерирование, циклы и итераторы в JavaScript

  • 📅 2018-11-05 · YouTube
  • Тезисы: Iterator protocol: Symbol.iterator возвращает { next }, который возвращает { value, done }. Iterable — то, у чего есть Symbol.iterator. Все встроенные коллекции (Array, Map, Set, String, NodeList) — iterable

🎓 Источник: Архив 2018 - Часть 5 EventEmitter, Symbol, Proxy

  • 📅 2020-01-05 · YouTube
  • Тезисы: Symbol как private-key обходится через Object.getOwnPropertySymbols. Полноценная приватность только через WeakMap или #field (private class field)