Symbol: уникальные ключи

объекты≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Обычный ключ свойства — строка, и две строки "id" — это всегда один и тот же ключ. Symbol даёт ключи другого сорта: каждый символ уникален и не совпадает ни с одним другим, даже если выглядит точно так же.

Разделы темы:·

Символ — примитивный тип, как число или строка. Создаётся вызовом Symbol(), и каждый вызов рождает новое значение, не равное никакому другому. Это единственное, что символ умеет, и вся его польза растёт отсюда.

Аргумент в скобках — описание, и оно нужно только человеку: попадает в отладочный вывод и в свойство description, но на сравнение не влияет никак. Это подпись на коробке, а не её содержимое — две коробки с одинаковой подписью остаются разными коробками.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Писать new Symbol() нельзя — будет ошибка. Символ не объект, конструктора у него нет, только функция-создатель.

В отличие от числа или null, символ не превращается в строку автоматически: "ключ: " + id бросит TypeError. Так язык страхует от опечатки, в которой уникальное значение молча схлопывается в текст. Нужен текст — попросите явно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ключом свойства в JavaScript может быть строка или символ — третьего варианта нет. Символьный ключ пишут в квадратных скобках: и в литерале объекта, и при чтении. Точечная запись тут не сработает, потому что после точки идёт имя, а символ — это значение из переменной.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Главное следствие — такой ключ невозможно занять дважды по неосторожности. Две библиотеки, обе решившие пометить ваш объект полем id, затрут работу друг друга и вы узнаете об этом из странного бага. Два символа с описанием "id" мирно живут рядом: они разные значения, а значит и разные ключи.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Свойства с символьными ключами не участвуют в обычном обходе объекта. for..in их не назовёт, Object.keys не вернёт, JSON.stringify не сериализует. Чужой код, который перебирает ваш объект, просто не узнает, что там что-то есть.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

«Невидимость» не абсолютная: часть операций символьные ключи всё же копирует и показывает. Полная картина умещается в шесть строк.

ОперацияВидит символьные ключи
for..inнет
Object.keys / values / entriesнет
JSON.stringifyнет
{ ...obj } и Object.assignда
Object.getOwnPropertySymbolsда
Reflect.ownKeysда

Спред и Object.assign делают честную копию всех собственных свойств, включая символьные. Это удобно (метка переживает копирование) и это же стоит держать в голове, если вы рассчитывали на обратное.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Несколько символов язык создал заранее и положил прямо в Symbol: Symbol.iterator, Symbol.toPrimitive, Symbol.toStringTag и другие. Это не секретные ключи, а точки подключения: в определённый момент движок сам заглядывает в объект по такому ключу и спрашивает, как с этим объектом поступить.

Самый частый — Symbol.iterator. Его вызывают for..of и спред .... Пока метода нет, объект перебрать нельзя; как только появился — можно, и никакой магии больше не требуется. Звёздочка перед именем ниже делает функцию генератором: такая функция отдаёт значения по одному, и каждое yield — очередное значение перебора. Для нашей цели этого достаточно: генератор здесь просто самый короткий способ написать итератор руками.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

По той же схеме Symbol.toPrimitive отвечает на вопрос «во что превратить объект, если его сложили или вставили в шаблон». Аргумент hint говорит, чего от объекта хотят: "number", "string" или "default".

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Сценарий первый — метка на чужом объекте. Вы получили данные из библиотеки или от сервера и хотите приписать к ним своё служебное поле так, чтобы оно не сломало сериализацию и не столкнулось с чужим именем. Второй — подключение к языку через системные символы. Третий — набор состояний, которые нельзя случайно сравнить со строкой: const LOADING = Symbol("loading") совпадёт только сам с собой.

Если одному и тому же символу нужно быть общим на весь код — есть глобальный реестр. Symbol.for("ключ") при первом вызове создаёт символ и запоминает, при следующих возвращает тот же самый, хоть из другого файла. Обычный Symbol() в реестр не попадает никогда.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Символ прячет свойство от случайного взгляда, но не от того, кто ищет: Object.getOwnPropertySymbols вернёт всё. Это неконфликтность, а не приватность. Нужна настоящая — берите #поля класса.

И обратная сторона: для обычных полей своего же объекта символы не нужны. Данные, которые уходят в JSON, конфигурация, всё, что удобно читать глазами в отладчике, — там строковый ключ проще и честнее. Символ достают, когда есть настоящий риск столкновения имён или требуется договориться с самим языком.

  • Symbol() при каждом вызове создаёт новое уникальное значение; описание в скобках — подпись для отладки, на равенство оно не влияет.
  • Ключом свойства может быть строка или символ. Символьный ключ пишут в квадратных скобках, и занять его случайно нельзя.
  • Символьные свойства не видят for..in, Object.keys и JSON.stringify, но копируют спред и Object.assign, а показывает Object.getOwnPropertySymbols.
  • Системные символы (Symbol.iterator, Symbol.toPrimitive) — точки подключения к языку: по ним движок сам спрашивает объект, как с ним обращаться.
  • Symbol.for("ключ") возвращает символ из глобального реестра — один и тот же для всего кода, в отличие от обычного Symbol().

1/3Что выведет console.log(Symbol("x") === Symbol("x"))?

2/3Объявлено const obj = { a: 1, [Symbol("b")]: 2 }. Что вернёт Object.keys(obj)?

3/3Symbol.for("id") вызвали дважды в разных файлах. Что получится?

Скрытая метка на чужом объекте

Объект user пришёл из чужого кода и уходит обратно в JSON. Пометьте его как уже обработанный так, чтобы JSON.stringify и Object.keys ничего не заметили, а вы могли прочитать метку в любой момент.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ключом берём символ. Свойство существует и читается, но ни сериализация, ни перебор его не видят — а чужая библиотека не сможет случайно занять то же имя.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Строковый ключ "visited" здесь не подошёл бы: он попал бы в JSON и в Object.keys.

Сделать объект перебираемым

У объекта team есть массив members, но сам объект спред не берёт. Добавьте Symbol.iterator, чтобы [...team] отдавал имена участников.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Писать итератор с нуля не нужно: у массива он уже есть, достаточно вернуть его наружу.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вариант через генератор читается не хуже: *[Symbol.iterator]() { yield* this.members; }. Оба работают и с for..of.