ES6: символы изнутри
Перевод статьи ES6 Symbols in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Приветствую! Милости просим в «ES6 — надо же, еще один выпуск — изнутри». Если не представляете, как тут очутились или вообще что такое ES6, рекомендую ознакомиться с краткой историей инструментария ES6. Затем изучить деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, let
, const
и «Временную мёртвую зону», а также итераторы и генераторы. Ну а сегодня поговорим о символах.
Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.
Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.
Спасибо, что выслушали, а теперь перейдём к символам! Мы уже познакомились вскользь с символами в статьях про итераторы и генераторы, так можете заглянуть туда.
Что такое символы?
Символы — новый примитивный тип в ES6. Как по мне, так они ужасно похожи на строки. Как и в случае с числами и строками, вместе с символами вводится соответствующий объект-обёртка Symbol
.
Можно создавать наши собственные символы.
var mystery = Symbol()
Заметьте, что оператор new
отсутствует. Он даже выбрасывает TypeError
, когда мы пробуем его с Symbol
.
var oops = new Symbol() // <- TypeError
В отладочных целях символы можно описывать.
var mystery = Symbol('это наглядное описание')
Символы неизменяемы. Так же, как числа или строки. Однако, заметьте, что символы уникальны, в отличие от примитивных чисел или строк.
console.log(Symbol() === Symbol()) // <- false console.log(Symbol('foo') === Symbol('foo')) // <- false
У символов тип symbol.
console.log(typeof Symbol()) // <- 'symbol' console.log(typeof Symbol('foo')) // <- 'symbol'
Есть три вида символов — и обращаться к каждому из них нужно по-своему. Рассмотрим каждый из них и потихоньку выясним, что всё это значит.
- Доступ к локальным символам можно получить с помощью ссылки на них напрямую.
- Можно разместить символы в глобальном регистре и получить к ним доступ сквозь владения (realms).
- «Известные» символы не ограничены владениями – но нет возможности их создать, равно как их нет и в глобальном регистре.
Что ещё за владения, спросите вы? Владение — так на жаргоне спецификаций называют любой контекст исполнения, например, страницу, на которой запускается ваше приложение, или <iframe>
на странице.
«Всеконтекстный» регистр символов
Есть два метода для добавления символов к вcеконтекстному регистру символов. Symbol.for(key)
и Symbol.keyFor(symbol)
. Что они делают?
Symbol.for(key)
Этот метод ищет ключ (key
) во всеконтекстном регистре символов. Если символ с этим ключом есть в глобальном регистре, то этот символ и вернется. Если символ с таким ключом не найден в регистре, то он создаётся. Т.е. Symbol.for(key)
всегда возвращает один и тот же результат. В коде ниже первый вызов Symbol.for('foo')
создаёт символ, добавляет его в регистр и возвращает его. Второй вызов возвращает тот же символ, поскольку key
уже в регистре — и связан с символом, возвращённым первым вызовом.
Symbol.for('foo') === Symbol.for('foo') // <- true
Это с трудом вяжется с тем, что символы, как мы знаем, уникальны. Однако, глобальный регистр символов отслеживает символы по ключу. Заметьте, что ключ будет использоваться и в качестве описания (description
), когда создаются символы, попадающие в регистр. Также учитывайте, что эти символы глобальнее чем что бы то ни было в JS, так что ведите себе прилично и используйте префикс, а не просто называйте символы 'user'
или подобным общим названием.
Symbol.keyFor(symbol)
Если взять символ symbol
, Symbol.keyFor(symbol)
возвращает ключ, который был связан с symbol
при добавлении символа в глобальный регистр.
var symbol = Symbol.for('foo') console.log(Symbol.keyFor(symbol)) // <- 'foo'
Насколько широка всеконтекстность?
Всеконтекстность означает, что символы в глобальном регистре не ограничены контекстом исполнения кода. Вероятно, кусок кода лучше это прояснит. Это просто означает, что регистр общий для разных контекстов исполнения.
var frame = document.createElement('iframe') document.body.appendChild(frame) console.log(Symbol.for('foo') === frame.contentWindow.Symbol.for('foo')) // <- true
«Известные» символы
Успокою вас: на самом деле никакие они не известные. Отнюдь нет. Ещё несколько месяцев назад я и понятия не имел об этих вещах. Почему же их тогда называют «известными»? А потому, что они встроены в JavaScript и используются для управления частями языка. Они были недосягаемыми для пользовательского кода до ES6, но теперь это не так.
Отличный пример «известного» символа был у нас как-то на Pony Foo: известный символ Symbol.iterator
. Мы использовали его для определения метода @@iterator
в объекте, который придерживается протокола «Итератор». Есть ещё список известных символов на MDN, но на момент написания этой статьи описано всего несколько.
Один из известных символов, описанный в данный момент — Symbol.match
. Согласно MDN, можно установить свойству Symbol.match
регулярных выражений значение false
и заставить их при сравнении вести себя, как литералы строки (вместо регулярных выражений, которые не особо дружат с .startsWith
, .endsWith
или .includes
).
Эта часть спецификации ещё не реализована в Babel — полагаю потому, что игра не стоит свеч — но по идее, это должно быть как-то так.
var text = '/foo/' var literal = /foo/ literal[Symbol.match] = false console.log(text.startsWith(literal)) // <- true
В чем прикол делать так вместо того, чтобы привести литерал (literal
) к строке — загадка для меня.
var text = '/foo/' var casted = /foo/.toString() console.log(text.startsWith(casted)) // <- true
Наверное, наличие этого символа в языке оправдано какими-то важными соображениями производительности, но не думаю, что это станет основой front-end разработки в ближайшее время.
Тем не менее
Symbol.iterator
на самом деле очень полезный, уверен, что и другие известные символы также полезны.
Заметьте, что известные символы уникальны, но общие для разных контекстов исполнения, даже при том, что они не доступны в глобальном регистре.
var frame = document.createElement('iframe') document.body.appendChild(frame) console.log(Symbol.iterator === frame.contentWindow.Symbol.iterator) // <- true
Не доступны в глобальном регистре? Неа!
console.log(Symbol.keyFor(Symbol.iterator)) // <- undefined
Хотя, возможности обратиться к ним статически откуда угодно должно быть вполне достаточно.
Символы и итераторы
Всё, что работает с протоколом «Итерируемый» явно игнорирует все символы кроме Symbol.iterator
, который показывает, что объект итерируемый, и определяет, как его перебирать.
var foo = { [Symbol()]: 'foo', [Symbol('foo')]: 'bar', [Symbol.for('bar')]: 'baz', что: 'угодно' } console.log([...foo]) // <- []
Метод Object.keys
из ES5 игнорирует символы.
console.log(Object.keys(foo)) // <- ['что']
То же касается и JSON.stringify
.
console.log(JSON.stringify(foo)) // <- {"что":"угодно"}
Ну что, тогда for..in
? Неа.
for (let key in foo) { console.log(key) // <- 'что' }
А, Object.getOwnPropertyNames
. Тоже нет! — Но близко.
console.log(Object.getOwnPropertyNames(foo)) // <- ['что']
Вам нужно явно искать символы, чтобы к ним обратиться. Они как нейтрино в JavaScript. Для их обнаружения воспользуйтесь Object.getOwnPropertySymbols
.
console.log(Object.getOwnPropertySymbols(foo)) // <- [Symbol(), Symbol('foo'), Symbol.for('bar')]
Магическая завеса с символов спадает, и теперь можно перебирать символы с помощью цикла for..of
, чтобы наконец найти сокровища, которые они охраняли. Надеюсь, в вашем случае эта находка окажется поинтереснее, чем в примере ниже.
for (let symbol of Object.getOwnPropertySymbols(foo)) { console.log(foo[symbol]) // <- 'foo' // <- 'bar' // <- 'baz' }
Зачем символы мне?
Есть несколько разных применений для символов.
Конфликты имён
С помощью символов можно избежать конфликта имён в ключах свойств. Это важно, когда речь идет о паттерне «объект — это ассоциативный массив», с которым то и дело случается досадный облом, как только кто-то (случайно или по злому умыслу) переопределяет нативные методы.
«Приватность»?
Символы невидимы для всех reflection-методов до ES6. В некоторых случаях это может быть полезно, но они не приватны при всём желании, как мы только что продемонстрировали с помощью API Object.getOwnPropertySymbols
.
С другой стороны, тот факт, что символы приходится искать специально, означает, что они полезны в сценариях, где нужно определить метаданные, которые не должны быть частью итерируемых последовательностей для массива или любых итерируемых объектов.
Определение протоколов
Думаю, наибольшая польза от символов именно в том, для чего реализации ES6 их используют: определение протоколов — как Symbol.iterator
, определяющий, как может итерироваться объект.
Представьте, например, библиотеку типа dragula
, определяющую протокол через Symbol.for('dragula.moves')
, где можно было бы добавлять метод этого Symbol
к любым DOM-элементам. Если бы DOM-элемент подчинялся протоколу, то dragula
могла бы вызвать пользовательский метод el[Symbol.for('dragula.moves')]()
, чтобы решить, можно ли этот элемент двигать.
Таким образом, логика перетаскиваемых dragula
элементов переносится из одного места для целого объекта drake
(параметр options для экземпляра dragula
) на каждый отдельный DOM-элемент. Это упрощает работу со сложными взаимодействиями в больших проектах, поскольку логика передаётся отдельным нодам в DOM, а не сосредоточена в одном методе options.moves
.
P.S. Это тоже может быть интересно: