По спецификации только два примитивных типа могут служить ключами свойств объекта:
тип строки или
тип символа.
В противном случае, если используется другой тип, например число, он автоматически преобразуется в строку. Таким образом, obj[1]
совпадает с obj["1"]
, а obj[true]
совпадает с obj["true"]
.
До сих пор мы использовали только строки.
Теперь давайте исследуем символы, посмотрим, что они могут для нас сделать.
«Символ» представляет собой уникальный идентификатор.
Значение этого типа можно создать с помощью Symbol()
:
пусть идентификатор = Символ();
При создании мы можем дать символам описание (также называемое именем символа), что в основном полезно для целей отладки:
// id — символ с описанием «id» пусть id = Символ("ID");
Символы гарантированно уникальны. Даже если мы создадим множество символов с одинаковым описанием, они будут иметь разные значения. Описание — это просто ярлык, который ни на что не влияет.
Например, вот два символа с одинаковым описанием – они не равны:
пусть id1 = Символ("id"); пусть id2 = Символ("id"); оповещение (id1 == id2); // ЛОЖЬ
Если вы знакомы с Ruby или другим языком, в котором тоже есть какие-то «символы», не заблуждайтесь. Символы JavaScript разные.
Итак, подведем итог: символ — это «примитивное уникальное значение» с необязательным описанием. Давайте посмотрим, где мы можем их использовать.
Символы не преобразуются автоматически в строку
Большинство значений в JavaScript поддерживают неявное преобразование в строку. Например, мы можем alert
практически о любом значении, и это сработает. Символы особенные. Они не конвертируются автоматически.
Например, это alert
покажет ошибку:
пусть id = Символ("ID"); оповещение (идентификатор); // Ошибка типа: невозможно преобразовать значение символа в строку
Это «языковая защита» от ошибок, поскольку строки и символы фундаментально различны и не должны случайно превращаться друг в друга.
Если мы действительно хотим отобразить символ, нам нужно явно вызвать для него .toString()
, как здесь:
пусть id = Символ("ID"); оповещение(id.toString()); // Символ(id), теперь работает
Или получите symbol.description
, чтобы отображалось только описание:
пусть id = Символ("ID"); оповещение(id.description); // идентификатор
Символы позволяют нам создавать «скрытые» свойства объекта, к которым никакая другая часть кода не сможет случайно получить доступ или перезаписать их.
Например, если мы работаем с user
объектами, принадлежащими стороннему коду. Мы хотели бы добавить к ним идентификаторы.
Давайте воспользуемся для этого символьным ключом:
let user = { // принадлежит другому коду имя: «Джон» }; пусть id = Символ("ID"); пользователь[id] = 1; оповещение(пользователь[id]); // мы можем получить доступ к данным, используя символ в качестве ключа
В чем преимущество использования Symbol("id")
вместо строки "id"
?
Поскольку user
объекты принадлежат другой базе кода, добавлять к ним поля небезопасно, поскольку мы можем повлиять на заранее определенное поведение в этой другой базе кода. Однако к символам нельзя получить доступ случайно. Сторонний код не будет знать о вновь определенных символах, поэтому можно безопасно добавлять символы в user
объекты.
Кроме того, представьте, что другой скрипт хочет иметь свой собственный идентификатор внутри user
для своих целей.
Затем этот скрипт может создать свой собственный Symbol("id")
, например:
// ... пусть id = Символ("ID"); user[id] = "Значение их идентификатора";
Конфликта между нашими и их идентификаторами не будет, потому что символы всегда разные, даже если у них одинаковое имя.
…Но если бы мы использовали для той же цели строку "id"
вместо символа, то возник бы конфликт:
let user = { name: "Джон" }; // Наш скрипт использует свойство "id" user.id = "Значение нашего идентификатора"; // ...Другому скрипту для своих целей также нужен "id"... user.id = "Значение их идентификатора" // Бум! перезаписан другим скриптом!
Если мы хотим использовать символ в литерале объекта {...}
, нам нужны квадратные скобки вокруг него.
Так:
пусть id = Символ("ID"); пусть пользователь = { имя: «Джон», [id]: 123 // не «id»: 123 };
Это потому, что нам нужно значение переменной id
в качестве ключа, а не строка «id».
Символические свойства не участвуют в цикле for..in
.
Например:
пусть id = Символ("ID"); пусть пользователь = { имя: «Джон», возраст: 30, [идентификатор]: 123 }; for (введите пользователя) alert(key); // имя, возраст (без символов) // прямой доступ по символу работает alert("Прямой: " + пользователь[id]); // Прямой: 123
Object.keys(user) также их игнорирует. Это часть общего принципа «сокрытия символических свойств». Если другой скрипт или библиотека зацикливается на нашем объекте, он не получит неожиданного доступа к символическому свойству.
Напротив, Object.assign копирует свойства как строки, так и символа:
пусть id = Символ("ID"); пусть пользователь = { [идентификатор]: 123 }; пусть клон = Object.assign({}, пользователь); оповещение(клон[id]); // 123
Здесь нет никакого парадокса. Это задумано. Идея состоит в том, что когда мы клонируем объект или объединяем объекты, мы обычно хотим, чтобы все свойства были скопированы (включая такие символы, как id
).
Как мы видели, обычно все символы разные, даже если они имеют одно и то же имя. Но иногда нам хочется, чтобы символы с одинаковыми именами представляли собой одни и те же объекты. Например, разные части нашего приложения хотят получить доступ к символу "id"
означающему одно и то же свойство.
Для этого существует глобальный реестр символов . Мы можем создавать в нем символы и обращаться к ним позже, и это гарантирует, что повторные обращения по одному и тому же имени вернут точно тот же символ.
Чтобы прочитать (создать, если он отсутствует) символ из реестра, используйте Symbol.for(key)
.
Этот вызов проверяет глобальный реестр и, если есть символ, описанный как key
, возвращает его, в противном случае создается новый символ Symbol(key)
и сохраняется в реестре по заданному key
.
Например:
// читаем из глобального реестра пусть id = Symbol.for("id"); // если символ не существовал, он создается // читаем его еще раз (возможно, из другой части кода) пусть idAgain = Символ.for("id"); // тот же символ Предупреждение (идентификатор === idAgain); // истинный
Символы внутри реестра называются глобальными символами . Если нам нужен символ всего приложения, доступный повсюду в коде — они для этого и нужны.
Это похоже на Руби
В некоторых языках программирования, например Ruby, для каждого имени используется один символ.
В JavaScript, как мы видим, это справедливо для глобальных символов.
Мы видели, что для глобальных символов Symbol.for(key)
возвращает символ по имени. Чтобы сделать обратное — вернуть имя по глобальному символу — мы можем использовать: Symbol.keyFor(sym)
:
Например:
// получаем символ по имени let sym = Символ.for("имя"); пусть sym2 = Символ.for("id"); // получаем имя по символу Предупреждение(Symbol.keyFor(sym)); // имя предупреждение(Symbol.keyFor(sym2)); // идентификатор
Symbol.keyFor
внутренне использует глобальный реестр символов для поиска ключа символа. Поэтому это не работает для неглобальных символов. Если символ не является глобальным, он не сможет его найти и вернет undefined
.
При этом все символы имеют свойство description
.
Например:
let globalSymbol = Символ.for("имя"); пусть localSymbol = Символ("имя"); предупреждение(Symbol.keyFor(globalSymbol)); // имя, глобальный символ Предупреждение(Symbol.keyFor(localSymbol)); // неопределенное, не глобальное предупреждение (localSymbol.description); // имя
Существует множество «системных» символов, которые JavaScript использует внутри себя, и мы можем использовать их для тонкой настройки различных аспектов наших объектов.
Они указаны в спецификации в таблице Общеизвестные символы:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
…и так далее.
Например, Symbol.toPrimitive
позволяет нам описывать преобразование объекта в примитив. Очень скоро мы увидим его применение.
Другие символы также станут нам знакомы, когда мы изучим соответствующие особенности языка.
Symbol
— это примитивный тип уникальных идентификаторов.
Символы создаются с помощью вызова Symbol()
с необязательным описанием (именем).
Символы всегда имеют разные значения, даже если они имеют одно и то же имя. Если мы хотим, чтобы одноимённые символы были равными, нам следует использовать глобальный реестр: Symbol.for(key)
возвращает (при необходимости создаёт) глобальный символ с key
в качестве имени. Множественные вызовы Symbol.for
с одним и тем же key
возвращают один и тот же символ.
Символы имеют два основных варианта использования:
«Скрытые» свойства объекта.
Если мы хотим добавить свойство к объекту, который «принадлежит» другому скрипту или библиотеке, мы можем создать символ и использовать его в качестве ключа свойства. Символическое свойство не отображается в for..in
, поэтому оно не будет случайно обработано вместе с другими свойствами. Также к нему не будет прямого доступа, поскольку в другом скрипте нашего символа нет. Таким образом, свойство будет защищено от случайного использования или перезаписи.
Так мы можем «скрыто» спрятать в объекты что-то, что нам нужно, но не должны видеть другие, используя символические свойства.
В JavaScript используется множество системных символов, которые доступны как Symbol.*
. Мы можем использовать их, чтобы изменить некоторые встроенные модели поведения. Например, позже в этом руководстве мы будем использовать Symbol.iterator
для итераций, Symbol.toPrimitive
для настройки преобразования объекта в примитив и так далее.
Технически символы не скрыты на 100%. Существует встроенный метод Object.getOwnPropertySymbols(obj), который позволяет нам получить все символы. Также существует метод Reflect.ownKeys(obj), который возвращает все ключи объекта, включая символические. Но большинство библиотек, встроенных функций и синтаксических конструкций не используют эти методы.