ES6 в деталях: символы
ES6 в деталях — это цикл статей о новых возможностях языка программирования JavaScript, появившихся в 6 редакции стандарта ECMAScript, кратко — ES6.
Что такое символы в ES6?
Символы — это не картинки.
Это и не смайлы, которые вы можете использовать в коде.
let 😻 = 😺 × 😍; // SyntaxError
Они также не являются литературным приёмом для описания чего-либо.
И, безусловно, это не кимвалы.
Так что же такое символы?
Седьмой тип данных
С тех пор как JavaScript был стандартизирован в 1997 году, в нем было 6 типов данных. До появления ES6 каждое значение в JS-приложении имело один из следующих типов:
- Undefined
- Null
- Boolean
- Number
- String
- Object
Каждый тип данных представляет собой набор значений. Первые пять наборов конечны. Например, существует только два значения Boolean
— true
и false
и они не порождают новые. С другой стороны, у Number
и String
существует большое количество значений. Стандарт говорит, что существует 18437736874454810627 значений типа Number
(включая NaN
— он относится к типу данных Number
, несмотря на то, что его название расшифровывается как «Не число»)
. Это ничто по сравнению с числом различных возможных значений типа String
, которых я думаю (2^(144 115 188 075 855 872) − 1) ÷ 65 535 … хотя я, возможно, просчитался.
Набор значений типа данных Object
неограничен. Каждый объект является уникальным. Каждый раз при открытии веб-страницы создаётся множество новых объектов.
ES6-символы — это значения, которые не являются ни строками, ни объектами. Они что-то новое — седьмой тип данных.
Давайте поговорим о том, где их использование могло бы нам пригодиться.
Один простой boolean
Иногда бывает очень удобно спрятать некоторые дополнительные данные в JavaScript-объект, который в действительности принадлежит кому-то другому.
Предположим, что вы пишете JS-библиотеку, которая использует CSS-переходы при перемещении элементов DOM по экрану. Попытка применить несколько последовательных переходов CSS к одному div
работает некорректно, это вызывает некрасивые, прерывистые «прыжки». Вы, конечно, думаете, что можете это исправить, но для этого сначала нужно найти способ проверить движется ли элемент в данный момент времени.
Как можно решить эту проблему?
Одним из возможных путей решения является использование CSS API для определения движения элемента в браузере. Но это перебор. Библиотека должна сама знать движется ли элемент.
Всё что требуется — это найти способ определять, какие из элементов движутся. Можете держать массив всех движущихся элементов. Каждый раз, когда библиотека вызывается для анимации элемента, можно просто искать этот элемент в массиве.
Хмм … но линейный поиск достаточно медленный, если массив большой.
Другой вариант — просто установить флаг на элементе:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
При таком подходе существует несколько потенциальных проблем. Все они связаны с тем, что c DOM может работать не только ваш код.
- Чей-то код, использующий
for-in
илиObject.keys()
, может наткнуться на созданное вами свойство и что-то сломается. - Какой-то умник мог додуматься до этого подхода первым и одновременная работа ваших скриптов приведет к конфликтам.
- Ещё какой-нибудь умник может додуматься до этого подхода в будущем, что тоже приведет к конфликтам.
- Комитет стандартизации может решить добавить метод
.isMoving()
всем элементам. Тогда вы вообще попали!
Конечно, последние три проблемы можно решить, сделав название свойства таким длинным и дурацким, что никто в жизни его не станет так называть:
if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
Не стоит на этом останавливаться.
Можно генерировать практически уникальное имя свойства, используя криптографию:
// получить 1024 Юникод символа абракадабры
var isMoving = SecureRandom.generateName();
…
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
Cинтаксис object[name]
позволяет использовать буквально любую строчку в качестве имени свойства. Это будет работать: коллизии практически невозможны, и код выглядит намного лучше.
Но, с другой стороны, такой синтаксис приводит к проблемам при отладке кода. Каждый раз, когда вы делаете console.log()
для элемента с таким свойством, вы будете получать длинную бессмысленную строку. А если таких свойств должно быть много? Как с этим работать? При каждой перезагрузке имена будут разные.
Разве это так сложно? Нам нужен просто один маленький boolean
!
Символы это решение
Символы — это значения, которые программа может создать и использовать в качестве названия свойств без риска пересечения пространств имен.
var mySymbol = Symbol();
Вызывая Symbol()
вы создаёте новый символ, значение которого не равно любому другому объекту.
Так же, как и строчку из цифр, вы можете использовать символ как имя свойства. Оно уникально, и поэтому свойство с таким именем гарантированно не будет повторяться с любым другим свойством.
obj[mySymbol] = "ok!"; // гарантированно уникально
console.log(obj[mySymbol]); // ok!
Здесь видно, как можно использовать символ в ситуации, описанной выше:
// создать уникальный символ
var isMoving = Symbol("isMoving");
…
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
Несколько замечаний по этому коду:
- Строка
"isMoving"
вSymbol("isMoving")
— это «описание». Это облегчает отладку. Это значение отображается, когда вы выводите символ на консоль и когда конвертируете символ в строку с помощью.toString()
, а также в возможных сообщениях ошибок. element[isMoving]
— свойство с ключом-символом. Это просто свойство, у которого имя — это не строка, а символ.- Так же как и к элементам массивов, к свойствам с ключами-символами нельзя обращаться через точку, как
obj.name
. К ним нужно обращаться используя квадратные скобки. - Если есть символ, то получить свойство с ключом-символом достаточно просто. Пример выше показывает, как получить и задать значение
element[isMoving]
, и мы так же можем проверить есть ли такое свойствоif (isMoving in element)
или даже удалить егоdelete element[isMoving]
, если потребуется. - С другой стороны, всё это возможно до тех пор, пока
isMoving
находится в текущей области видимости. Символы отлично подходят для слабой инкапсуляции: модуль, который создал несколько символов, может использовать их с любыми объектами без опасности пересечения пространств имен.
Так как символы были созданы для того, чтобы избежать коллизий, наиболее распространённые способы просмотра свойств объектов в JavaScript просто игнорируют ключи-символы. Например, в цикле for-in
происходит перебор всех строчных свойств. Свойства с символами в качестве имен игнорируются. Object.keys(obj)
и Object.getOwnPropertyNames(obj)
делают то же самое. Но символы не совсем приватны: с помощью нового API Object.getOwnPropertySymbols(obj)
можно получить список ключей-символов объекта. Ещё один новый API, Reflect.ownKeys(obj)
, возвращает все свойства: с ключами-строками, и ключами-символами. (Мы обсудим Reflect
подробнее в следующих статьях).
Библиотеки и фреймворки скорее всего найдут символам множество применений, и, как мы убедимся в дальнейшем, язык сам по себе использует их в самых разных целях.
Итак, что такое символы?
> typeof Symbol()
"symbol"
Символы не похожи ни на что другое.
После создания символы невозможно изменить, то есть задать им свойства (в strict mode
это приведёт к TypeError
).
Символы могут быть именами свойств — этим они похожи на строки.
С другой стороны, каждый символ уникален, отличен от других символов (даже если они имеют то же описание), и можно легко создать новый символ. Так проявляется их схожесть с объектами.
Символы в ES6 похожи на традиционные символы, такие, как в языках Lisp и Ruby, но не настолько интегрированы в язык. В Lisp все идентификаторы являются символами. В JavaScript идентификаторы и большинство свойств по-прежнему являются строками. Символы играют вспомогательную роль.
Ещё одна особенность символов: в отличие от большинства типов данных языка, символы не могут автоматически конвертироваться в строку. Попытка конкатенации символа со строкой приведёт к TypeError.
> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: невозможно конвертировать символ в строку
> `your symbol is ${sym}`
// TypeError: невозможно конвертировать символ в строку
Избежать ошибки поможет явная конвертация символа в строку String(sym)
или sym.toString()
.
Три вида символов
Есть три способа получить символ.
- Вызов
Symbol()
. Как говорилось ранее, такой вызов будет каждый раз возвращать новый уникальный символ. - Вызов
Symbol.for(string)
. Это доступ к набору существующих символов под названием «реестр символов». В отличие от уникальных символов, определённых какSymbol()
, символы из реестра являются общими. Если вы вызоветеSymbol.for("cat")
тридцать раз, каждый раз он будет возвращать одинаковый символ. Реестр символов полезно использовать в случае, когда необходимо разделить символ между несколькими веб-страницами или несколькими модулями в пределах одной страницы. - Использование описанных в стандарте символов вроде
Symbol.iterator
. Некоторые символы определены в стандарте. Каждый из них имеет своё назначение.
Если вы всё ещё сомневаетесь в полезности символов, следующий раздел будет вам интересен, потому что в нём показано, как символы доказали свою полезность на практике.
Как символы используются в спецификации ES6
Мы уже видели одно применение символов в ES6 — избежание конфликтов с существующим кодом. Несколько недель назад, в статье с итераторами, мы видели, что цикл for (var item of myArray)
начинается с вызова myArray[Symbol.iterator]()
. Я упоминал, что можно использовать myArray.iterator()
, но символы предпочтительнее с точки зрения обратной совместимости.
Теперь, когда мы знаем всё о символах, легко понять, почему это было сделано и что это значит.
Перечислим несколько мест, где ES6 использует символы (эти фичи ещё не реализованы в Firefox).
- Делает
instanceof
расширяемым. В ES6 выражениеobject instanceof constructor
определено как метод конструктора:constructor[Symbol.hasInstance](object)
. Это значит, что он расширяемый. - Разрешение конфликтов между новым и старым кодом. Это действительно странно, но мы обнаружили, что некоторые методы
Array
в ES6 ломают существующие сайты самим фактом своего существования. С другим стандартами похожие проблемы: обычное добавление новых методов может сломать существующие сайты. Однако, поломка в основном вызвана чем-то называющимся динамической областью видимости, поэтому ES6 вводит специальный символ,Symbol.unscopables
, который можно использовать для предотвращения ситуаций, когда определённый метод присутствует в динамической области видимости. - Поддержка новых способов определения соответствий между строками. В ES5
str.match(myObject)
пыталось конвертироватьmyObject
вRegExp
. В ES6 сначала проверяется, имеет лиmyObject
методmyObject[Symbol.match](str)
. Теперь библиотеки могут обеспечить пользовательские методы обработки строк, которые работают везде, где работаетRegExp
.
Каждый из этих случаев довольно узкоспециализирован. Трудно понять, что из этого можно использовать в повседневной работе. В дальней перспективе всё несколько интереснее. Символы в JavaScript — это улучшенная версия __doubleUnderscores
из PHP и Python. Стандарт будет использовать их в будущем для добавления новых хуков в язык без риска поломать существующий код.
Когда я смогу использовать ES6 символы?
Символы уже имплементированы в Firefox 36 и Chrome 38. В Firefox это сделал я сам, поэтому если символы у вас не работают как положено, вы знаете к кому обратиться.
Для браузеров, которые ещё не имеют нативной поддержки символов, вы можете использовать полифиллы вроде core.js. Тем не менее символы не похожи ни на что из того, что было ранее в языке, поэтому полифиллы не совершенны. Обратите внимание на оговорки.