ES6 в деталях: символы

ES6 в деталях — это цикл статей о новых возможностях языка программирования JavaScript, появившихся в 6 редакции стандарта ECMAScript, кратко — ES6.

Что такое символы в ES6?

Символы — это не картинки.

Это и не смайлы, которые вы можете использовать в коде.

let 😻 = 😺 × 😍;  // SyntaxError

Они также не являются литературным приёмом для описания чего-либо.

И, безусловно, это не кимвалы.

Иллюстрация

Так что же такое символы?

Седьмой тип данных

С тех пор как JavaScript был стандартизирован в 1997 году, в нем было 6 типов данных. До появления ES6 каждое значение в JS-приложении имело один из следующих типов:

Каждый тип данных представляет собой набор значений. Первые пять наборов конечны. Например, существует только два значения Booleantrue и 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 может работать не только ваш код.

  1. Чей-то код, использующий for-in или Object.keys(), может наткнуться на созданное вами свойство и что-то сломается.
  2. Какой-то умник мог додуматься до этого подхода первым и одновременная работа ваших скриптов приведет к конфликтам.
  3. Ещё какой-нибудь умник может додуматься до этого подхода в будущем, что тоже приведет к конфликтам.
  4. Комитет стандартизации может решить добавить метод .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;

Несколько замечаний по этому коду:

Так как символы были созданы для того, чтобы избежать коллизий, наиболее распространённые способы просмотра свойств объектов в 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().

Три вида символов

Есть три способа получить символ.

Если вы всё ещё сомневаетесь в полезности символов, следующий раздел будет вам интересен, потому что в нём показано, как символы доказали свою полезность на практике.

Как символы используются в спецификации ES6

Мы уже видели одно применение символов в ES6 — избежание конфликтов с существующим кодом. Несколько недель назад, в статье с итераторами, мы видели, что цикл for (var item of myArray) начинается с вызова myArray[Symbol.iterator](). Я упоминал, что можно использовать myArray.iterator(), но символы предпочтительнее с точки зрения обратной совместимости.

Теперь, когда мы знаем всё о символах, легко понять, почему это было сделано и что это значит.

Перечислим несколько мест, где ES6 использует символы (эти фичи ещё не реализованы в Firefox).

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

Когда я смогу использовать ES6 символы?

Символы уже имплементированы в Firefox 36 и Chrome 38. В Firefox это сделал я сам, поэтому если символы у вас не работают как положено, вы знаете к кому обратиться.

Для браузеров, которые ещё не имеют нативной поддержки символов, вы можете использовать полифиллы вроде core.js. Тем не менее символы не похожи ни на что из того, что было ранее в языке, поэтому полифиллы не совершенны. Обратите внимание на оговорки.