Тип данных Symbol
По спецификации, в качестве ключей для свойств объекта могут использоваться только строки или символы. Ни числа, ни логические значения не подходят, разрешены только эти два типа данных.
До сих пор мы видели только строки. Теперь давайте разберём символы, увидим, что хорошего они нам дают.
Символы
«Символ» представляет собой уникальный идентификатор.
Создаются новые символы с помощью функции Symbol() :
// Создаём новый символ - id let
При создании, символу можно дать описание (также называемое имя), в основном использующееся для отладки кода:
// Создаём символ id с описанием (именем) "id" let
Символы гарантированно уникальны. Даже если мы создадим множество символов с одинаковым описанием, это всё равно будут разные символы. Описание – это просто метка, которая ни на что не влияет.
Например, вот два символа с одинаковым описанием – но они не равны:
let id1 = Symbol("id"); let id2 = Symbol("id"); alert(id1 == id2); // false
Если вы знаете Ruby или какой-то другой язык программирования, в котором есть своего рода «символы» – пожалуйста, будьте внимательны. Символы в JavaScript имеют свои особенности, и не стоит думать о них, как о символах в Ruby или в других языках.
Символы не преобразуются автоматически в строки
Большинство типов данных в JavaScript могут быть неявно преобразованы в строку. Например, функция alert принимает практически любое значение, автоматически преобразовывает его в строку, а затем выводит это значение, не сообщая об ошибке. Символы же особенные и не преобразуются автоматически.
К примеру, alert ниже выдаст ошибку:
let // TypeError: Cannot convert a Symbol value to a string
Это – языковая «защита» от путаницы, ведь строки и символы – принципиально разные типы данных и не должны неконтролируемо преобразовываться друг в друга.
Если же мы действительно хотим вывести символ с помощью alert , то необходимо явно преобразовать его с помощью метода .toString() , вот так:
let // Symbol(id), теперь работает
Или мы можем обратиться к свойству symbol.description , чтобы вывести только описание:
let // id
«Скрытые» свойства
Символы позволяют создавать «скрытые» свойства объектов, к которым нельзя нечаянно обратиться и перезаписать их из других частей программы.
Например, мы работаем с объектами user , которые принадлежат стороннему коду. Мы хотим добавить к ним идентификаторы.
Используем для этого символьный ключ:
let user = < name: "Вася" >; let = 1; alert( user[id] ); // мы можем получить доступ к данным по ключу-символу
Почему же лучше использовать Symbol(«id») , а не строку «id» ?
Так как объект user принадлежит стороннему коду, и этот код также работает с ним, то нам не следует добавлять к нему какие-либо поля. Это небезопасно. Но к символу сложно нечаянно обратиться, сторонний код вряд ли его вообще увидит, и, скорее всего, добавление поля к объекту не вызовет никаких проблем.
Кроме того, предположим, что другой скрипт для каких-то своих целей хочет записать собственный идентификатор в объект user . Этот скрипт может быть какой-то JavaScript-библиотекой, абсолютно не связанной с нашим скриптом.
Сторонний код может создать для этого свой символ Symbol(«id») :
// . let = "Их идентификатор";
Конфликта между их и нашим идентификатором не будет, так как символы всегда уникальны, даже если их имена совпадают.
А вот если бы мы использовали строку «id» вместо символа, то тогда был бы конфликт:
let user = < name: "Вася" >; // Объявляем в нашем скрипте свойство "id" user.id = "Наш идентификатор"; // . другой скрипт тоже хочет свой идентификатор. user.id = "Их идентификатор" // Ой! Свойство перезаписано сторонней библиотекой!
Символы в литеральном объекте
Если мы хотим использовать символ при литеральном объявлении объекта <. >, его необходимо заключить в квадратные скобки.
let user = < name: "Вася", [id]: 123 // просто "id: 123" не сработает >;
Это вызвано тем, что нам нужно использовать значение переменной id в качестве ключа, а не строку «id».
Символы игнорируются циклом for…in
Свойства, чьи ключи – символы, не перебираются циклом for..in .
let user = < name: "Вася", age: 30, [id]: 123 >; for (let key in user) alert(key); // name, age (свойства с ключом-символом нет среди перечисленных) // хотя прямой доступ по символу работает alert( "Напрямую: " + user[id] );
Это – часть общего принципа «сокрытия символьных свойств». Если другая библиотека или скрипт будут работать с нашим объектом, то при переборе они не получат ненароком наше символьное свойство. Object.keys(user) также игнорирует символы.
А вот Object.assign, в отличие от цикла for..in , копирует и строковые, и символьные свойства:
let user = < [id]: 123 >; let clone = Object.assign(<>, user); alert( clone[id] ); // 123
Здесь нет никакого парадокса или противоречия. Так и задумано. Идея заключается в том, что, когда мы клонируем или объединяем объекты, мы обычно хотим скопировать все свойства (включая такие свойства с ключами-символами, как, например, id в примере выше).
Глобальные символы
Итак, как мы видели, обычно все символы уникальны, даже если их имена совпадают. Но иногда мы наоборот хотим, чтобы символы с одинаковыми именами были одной сущностью. Например, разные части нашего приложения хотят получить доступ к символу «id» , подразумевая именно одно и то же свойство.
Для этого существует глобальный реестр символов. Мы можем создавать в нём символы и обращаться к ним позже, и при каждом обращении нам гарантированно будет возвращаться один и тот же символ.
Для чтения (или, при отсутствии, создания) символа из реестра используется вызов Symbol.for(key) .
Он проверяет глобальный реестр и, при наличии в нём символа с именем key , возвращает его, иначе же создаётся новый символ Symbol(key) и записывается в реестр под ключом key .
// читаем символ из глобального реестра и записываем его в переменную let // если символа не существует, он будет создан // читаем его снова и записываем в другую переменную (возможно, из другого места кода) let idAgain = Symbol.for("id"); // проверяем -- это один и тот же символ alert( idAgain ); // true
Символы, содержащиеся в реестре, называются глобальными символами. Если вам нужен символ, доступный везде в коде – используйте глобальные символы.
Похоже на Ruby
В некоторых языках программирования, например, Ruby, на одно имя (описание) приходится один символ, и не могут существовать разные символы с одинаковым именем.
В JavaScript, как мы видим, это утверждение верно только для глобальных символов.
Symbol.keyFor
Для глобальных символов, кроме Symbol.for(key) , который ищет символ по имени, существует обратный метод: Symbol.keyFor(sym) , который, наоборот, принимает глобальный символ и возвращает его имя.
// получаем символ по имени let sym = Symbol.for("name"); let sym2 = Symbol.for("id"); // получаем имя по символу alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id
Внутри метода Symbol.keyFor используется глобальный реестр символов для нахождения имени символа. Так что этот метод не будет работать для неглобальных символов. Если символ неглобальный, метод не сможет его найти и вернёт undefined .
Впрочем, для любых символов доступно свойство description .
let globalSymbol = Symbol.for("name"); let localSymbol = Symbol("name"); alert( Symbol.keyFor(globalSymbol) ); // name, глобальный символ alert( Symbol.keyFor(localSymbol) ); // undefined для неглобального символа alert( localSymbol.description ); // name
Системные символы
Существует множество «системных» символов, использующихся внутри самого JavaScript, и мы можем использовать их, чтобы настраивать различные аспекты поведения объектов.
Эти символы перечислены в спецификации в таблице Well-known symbols:
- Symbol.hasInstance
- Symbol.isConcatSpreadable
- Symbol.iterator
- Symbol.toPrimitive
- …и так далее.
В частности, Symbol.toPrimitive позволяет описать правила для объекта, согласно которым он будет преобразовываться к примитиву. Мы скоро увидим его применение.
С другими системными символами мы тоже скоро познакомимся, когда будем изучать соответствующие возможности языка.
Итого
Символ (symbol) – примитивный тип данных, использующийся для создания уникальных идентификаторов.
Символы создаются вызовом функции Symbol() , в которую можно передать описание (имя) символа.
Даже если символы имеют одно и то же имя, это – разные символы. Если мы хотим, чтобы одноимённые символы были равны, то следует использовать глобальный реестр: вызов Symbol.for(key) возвращает (или создаёт) глобальный символ с key в качестве имени. Многократные вызовы команды Symbol.for с одним и тем же аргументом возвращают один и тот же символ.
Символы имеют два основных варианта использования:
- «Скрытые» свойства объектов. Если мы хотим добавить свойство в объект, который «принадлежит» другому скрипту или библиотеке, мы можем создать символ и использовать его в качестве ключа. Символьное свойство не появится в for..in , так что оно не будет нечаянно обработано вместе с другими. Также оно не будет модифицировано прямым обращением, так как другой скрипт не знает о нашем символе. Таким образом, свойство будет защищено от случайной перезаписи или использования. Так что, используя символьные свойства, мы можем спрятать что-то нужное нам, но что другие видеть не должны.
- Существует множество системных символов, используемых внутри JavaScript, доступных как Symbol.* . Мы можем использовать их, чтобы изменять встроенное поведение ряда объектов. Например, в дальнейших главах мы будем использовать Symbol.iterator для итераторов, Symbol.toPrimitive для настройки преобразования объектов в примитивы и так далее.
Технически символы скрыты не на 100%. Существует встроенный метод Object.getOwnPropertySymbols(obj) – с его помощью можно получить все свойства объекта с ключами-символами. Также существует метод Reflect.ownKeys(obj), который возвращает все ключи объекта, включая символьные. Так что они не совсем спрятаны. Но большинство библиотек, встроенных методов и синтаксических конструкций не используют эти методы.
Symbol
Символ (анг. Symbol) — это уникальный и неизменяемый тип данных, который может быть использован как идентификатор для свойств объектов. Символьный объект (анг. symbol object) — это объект-обёртка (англ. wrapper) для примитивного символьного типа.
Синтаксис
Symbol([описание])
Параметры
описание Необязательный
Необязательный, строка. Описание символа, которое может быть использовано во время отладки, но не для доступа к самому символу.
Описание
Чтобы создать новый символьный примитив, достаточно написать Symbol() , указав по желанию строку в качестве описания этого символа:
var sym1 = Symbol(); var sym2 = Symbol("foo"); var sym3 = Symbol("foo");
Код выше создаёт три новых символа. Заметьте, что Symbol(«foo») не выполняет приведение (англ. coercion) строки «foo» к символу. Это выражение создаёт каждый раз новый символ:
Symbol("foo") === Symbol("foo"); // false
Код ниже с оператором new бросит исключение TypeError :
var sym = new Symbol(); // TypeError
Это удерживает разработчиков от создания явного объекта-обёртки Symbol вместо нового символьного значения. Создание явных объектов-обёрток для примитивных типов доступно (например, new Boolean , new String , new Number ).
Если вам действительно необходимо обернуть символ в объект, вы можете использовать функцию Object() :
var sym = Symbol("foo"); typeof sym; // "symbol" var symObj = Object(sym); typeof symObj; // "object"
Разделяемые символы в глобальном символьном реестре
Приведённый выше синтаксис, использующий функцию Symbol(), не создаст глобальный символ, который был бы доступен в любом месте вашего кода. Для создания символов, доступных во всех файлах и в окружении (глобальной области), используйте методы Symbol.for() и Symbol.keyFor() , чтобы задать или получить символ из глобального символьного реестра.
Поиск символьных свойств у объектов
Метод Object.getOwnPropertySymbols() возвращает массив символов и позволяет получить символьные свойства конкретного объекта. Следует заметить, что при инициализации объекты не получают символьных свойств, так что этот массив будет пуст, пока вы не зададите ему какое-либо символьное свойство.
Свойства
Содержит длину, всегда равную 0 (нулю).
Содержит прототип конструктора Symbol .
Известные символы
В добавок к вашим собственным символам, JavaScript имеет несколько встроенных символов, представляющих внутренние механизмы языка, которые не были доступны разработчикам в версиях ECMAScript 5 и более ранних. Эти символы доступны посредством следующих свойств:
Итерационные символы
Метод, возвращающий итератор по умолчанию для объекта. Используется конструкцией for. of .
Символы регулярных выражений
Метод для сопоставления объекта со строкой, также используемый для определения возможности объекта выступать в качестве регулярного выражения. Используется функцией String.prototype.match() .
Метод, заменяющий совпавшие подстроки в строке. Используется функцией String.prototype.replace() .
Метод, возвращающий индекс вхождения подстроки, соответствующей регулярному выражению. Используется функцией String.prototype.search() .
Метод, разбивающий строку на части в местах, соответствующих регулярному выражению. Используется функцией String.prototype.split() .
Другие символы
Метод, определяющий, распознает ли конструктор некоторый объект как свой экземпляр. Используется оператором instanceof .
Булево значение, показывающее, должен ли объект быть сведён к плоскому представлению (англ. flatten) в виде массива его элементов функцией Array.prototype.concat() .
Массив строковых имён свойств. Позволяет скрыть свойства от инструкции with (прежде всего для обратной совместимости).
Метод, определяющий конструктор для порождённых объектов.
Метод, преобразующий объект в примитив (примитивное значение).
Строковое значение, используемое в качестве описания объекта по умолчанию. Используется функцией Object.prototype.toString()
Методы
Ищет существующие символы по заданному ключу и возвращает его, если он найден. В противном случае создаётся новый символ для данного ключа в глобальном реестре символов.
Получает по разделяемому символу его ключ из глобального реестра символов.
Символ
Примитивный тип, представляющий собой уникальный идентификатор. Используется как имя свойств в объектах.
Время чтения: меньше 5 мин
Открыть/закрыть навигацию по статье
- Кратко
- Как пишется
- Создание
- Использование
- Системные символы
- Глобальный реестр символов
- Напишите свой класс, который бы являлся стратегией к функции split, используя Symbol. Пример:
Обновлено 20 декабря 2021
Кратко
Скопировать ссылку «Кратко» Скопировано
Символ ( Symbol ) — примитивный тип, значения которого создаются с помощью вызова функции Symbol . Каждый созданный символ уникален.
Символы могут использоваться в качестве имён свойств в объектах. Символьные свойства могут быть прочитаны только при прямом обращении и не видны при обычных операциях.
Как пишется
Скопировать ссылку «Как пишется» Скопировано
Создание
Скопировать ссылку «Создание» Скопировано
Для создания символа нужно вызвать функцию Symbol :
const sym = Symbol()const symTwo = Symbol() console.log(sym === symTwo)// falseconst sym = Symbol() const symTwo = Symbol() console.log(sym === symTwo) // falseСоздание символа через конструктор new Symbol ( ) не поддерживается.
При создании символа первым аргументом можно передать его описание. Оно ни на что не влияет и необходимо только для отладки. Даже если описания символов совпадают, JavaScript всё равно создаёт уникальные символы:
const sym = Symbol('name')const symTwo = Symbol('name') console.log(sym === symTwo)// falseconst sym = Symbol('name') const symTwo = Symbol('name') console.log(sym === symTwo) // falseИспользование
Скопировать ссылку «Использование» Скопировано
Символы используются для создания скрытых свойств объектов. В отличие от свойств, ключом которых является строка, символьные свойства может читать только владелец символа. Скрытые свойства не видны при его обходе с помощью for . . . in :
const secondaryId = Symbol() const user = 'id': 193, 'name': 'Ольга', [secondaryId]: 'olga-1'> for (const prop in user) console.log(prop, user[prop])>// id 193// name Ольга console.log(user[secondaryId])// olga-1const secondaryId = Symbol() const user = 'id': 193, 'name': 'Ольга', [secondaryId]: 'olga-1' > for (const prop in user) console.log(prop, user[prop]) > // id 193 // name Ольга console.log(user[secondaryId]) // olga-1Это может пригодиться, когда необходимо добавить свойства объекту, который могут модифицировать другие части программы. Таким образом только вы сможете читать созданное свойство, а гарантия уникальности символов гарантирует и отсутствие конфликтов имён.
Системные символы
Скопировать ссылку «Системные символы» Скопировано
Символы активно используются внутри самого JavaScript, чтобы определять поведение объектов. Такие символы называются «хорошо известными» (well-known symbols).
Самый известный символ Symbol . iterator , который позволяет реализовать обход конструкции с помощью синтаксических конструкций for . . of и спред-синтаксиса.
Полный список таких символов доступен в спецификации, но на практике он нужен нечасто.
Глобальный реестр символов
Скопировать ссылку «Глобальный реестр символов» Скопировано
Созданный символ уникален, но как быть, если он нужен в нескольких местах программы? Для решения этой проблемы существует глобальный реестр символов, он хранит символы по строковым ключам. При обращении по ключу всегда будет возвращаться один и тот же символ.
Работа с реестром символов организована с помощью двух методов:
- Symbol . for ( ключ ) — возвращает символ, хранящийся по ключу. Если символа ещё не существует, он создаётся автоматически.
- Symbol . keyFor ( символ ) — возвращает строковый ключ, который хранит переданный символ или undefined , если символ не хранится в реестре.
const secondaryId = Symbol() const user = 'id': 193, 'name': 'Ольга', [secondaryId]: 'olga-1'> console.log(Symbol.keyFor(secondaryId))// undefined const newSym = Symbol.for('registryKey')const newestSym = Symbol.for('registryKey')console.log(newSym === newestSym)// true user[newSym] = 'hello'console.log(Symbol.keyFor(newSym))// registryKeyconst secondaryId = Symbol() const user = 'id': 193, 'name': 'Ольга', [secondaryId]: 'olga-1' > console.log(Symbol.keyFor(secondaryId)) // undefined const newSym = Symbol.for('registryKey') const newestSym = Symbol.for('registryKey') console.log(newSym === newestSym) // true user[newSym] = 'hello' console.log(Symbol.keyFor(newSym)) // registryKeyНа собеседовании
Скопировать ссылку «На собеседовании» Скопировано
Напишите свой класс, который бы являлся стратегией к функции split, используя Symbol. Пример:
Скопировать ссылку «Напишите свой класс, который бы являлся стратегией к функции split, используя Symbol. Пример:» Скопировано
console.log('urlsomePath'.split(new Split1('url'))); // "url/somePath"console.log('somePathurlAnother'.split(new Split1('url'))); // "url/somePath/url/Another"console.log('urlsomePath'.split(new Split1('url'))); // "url/somePath" console.log('somePathurlAnother'.split(new Split1('url'))); // "url/somePath/url/Another"Скопировать ссылку «Danil Solomin отвечает» Скопировано
Что такое стратегия?
Скопировать ссылку «Что такое стратегия?» Скопировано
Стратегия (Strategy) — это ООП поведенческий шаблон проектирования, который позволяет расширять базовый класс или метод новым функционалом. Для этого нужно передать в него, так называемый, конкретный класс. Например:
const Clock = new Clock // Создаем базовую версию часовconst GoldenClock = new Clock(new GoldenParts) // Часы с золотым оформлениемconst WoodenClock = new Clock(new WoodenParts) // Часы с деревянным оформлением Clock.draw() // Рисуем стандартную версию часовGoldenClock.draw() // Метод тот же, что и строкой выше, но теперь мы используем золотое оформлениеWoodenClock.draw() // Используем деревянное оформлениеconst Clock = new Clock // Создаем базовую версию часов const GoldenClock = new Clock(new GoldenParts) // Часы с золотым оформлением const WoodenClock = new Clock(new WoodenParts) // Часы с деревянным оформлением Clock.draw() // Рисуем стандартную версию часов GoldenClock.draw() // Метод тот же, что и строкой выше, но теперь мы используем золотое оформление WoodenClock.draw() // Используем деревянное оформлениеВыполнение задачи
Скопировать ссылку «Выполнение задачи» Скопировано
Чтобы написать такой класс нам понадобится специальный well-known символ [ Symbol . split ] . Метод split ( ) вызывает функцию [ Symbol . split ] ( str ) , а результат вызова возвращает как результат split ( ) . У строк это уже реализовано:
"123,4,56".split(",") // ["123", "4", "56]"123,4,56".split(",") // ["123", "4", "56]Мы можем добавить такое же поведение к своему классу:
class MySplit constructor(value) this.value = value; // Принимаем строку по которой будем сплитить > [Symbol.split](string) < // принимаем строку-аргумент (которую будем сплитить) // В конце необходимо вернуть полученный результат, иначе split() вернет undefined >>class MySplit constructor(value) this.value = value; // Принимаем строку по которой будем сплитить > [Symbol.split](string) // принимаем строку-аргумент (которую будем сплитить) // В конце необходимо вернуть полученный результат, иначе split() вернет undefined > >Для нашей задачи разобьём строку при помощи регулярного выражения:
[Symbol.split](string) // Заменяем все вхождения this.value на /$/ let index = string.replace(new RegExp(this.value, "g"), `/$/`); // убираем первый слэш, /url/Path -> url/Path if (index[0] === "/") index = index.substr(1) // Строка должна начинаться с url/, даже если его не было в начале if (!index.startsWith(this.value)) index = `$/` + index; return index;>[Symbol.split](string) // Заменяем все вхождения this.value на /$/ let index = string.replace(new RegExp(this.value, "g"), `/$this.value>/`); // убираем первый слэш, /url/Path -> url/Path if (index[0] === "/") index = index.substr(1) // Строка должна начинаться с url/, даже если его не было в начале if (!index.startsWith(this.value)) index = `$this.value>/` + index; return index; >Пример работы
Скопировать ссылку «Пример работы» Скопировано
"foobarfoobaz".split(new MySplit("foo")) // "foo/var/foo/baz""foobarfoobaz".split(new MySplit("bar")) // "bar/foo/bar/foobaz""foobarfoobaz".split(new MySplit("foo")) // "foo/var/foo/baz" "foobarfoobaz".split(new MySplit("bar")) // "bar/foo/bar/foobaz"Что хотят проверить?
Скопировать ссылку «Что хотят проверить?» Скопировано
Этот вопрос проверяет умеете ли вы использовать символы. Хотя такие вопросы задают редко, из-за специфичности темы, умение пользоваться символами – полезный инструмент, который вам обязательно пригодится.
Особенности использования типа данных Symbol в JavaScript
Символьные примитивы — это одно из новшеств стандарта ES6, которое принесло в JavaScript некоторые ценные возможности. Символы, представленные типом данных Symbol, особенно полезны при использовании их в качестве идентификаторов свойств объектов. В связи с таким сценарием их применения напрашивается вопрос о том, что такого они могут, чего не могут строки.
В материале, перевод которого мы сегодня публикуем, речь пойдёт о типе данных Symbol в JavaScript. Начнём мы с обзора некоторых возможностей JavaScript, в которых нужно ориентироваться для того, чтобы разобраться с символами.
Предварительные сведения
В JavaScript, по сути, существует два вида значений. Первый вид — примитивные значения, второй — объектные (в их число входят и функции). Примитивные значения включают в себя простые типы данных наподобие чисел (сюда входит всё — от целых чисел, до чисел с плавающей точкой, значений Infinity и NaN ), логических значений, строк, значений undefined и null . Обратите внимание на то, что, хотя при проверке вида typeof null === ‘object’ получается true , null — это примитивное значение.
Примитивные значения иммутабельны. Их нельзя изменять. Конечно, в переменную, хранящую примитивное значение, можно записать что-то новое. Например, здесь выполняется запись нового значения в переменную x :
let x = 1; x++;Но при этом не происходит изменения (мутации) примитивного числового значения 1 .
В некоторых языках, например — в C, есть концепции передачи аргументов функций по ссылке и по значению. В JavaScript тоже есть нечто подобное. То, как именно организуется работа с данными, зависит от их типа. Если в функцию передают примитивное значение, представленное некоей переменной, а потом изменяют его в этой функции, значение, хранящееся в исходной переменной, при этом не меняется. Однако если в функцию передать объектное значение, представленное переменной, и модифицировать его, то изменится и то, что хранится в этой переменной.
Рассмотрим следующий пример:
function primitiveMutator(val) < val = val + 1; >let x = 1; primitiveMutator(x); console.log(x); // 1 function objectMutator(val) < val.prop = val.prop + 1; >let obj = < prop: 1 >; objectMutator(obj); console.log(obj.prop); // 2Примитивные значения (за исключением таинственного NaN , которое не равно самому себе) всегда оказываются равными другим примитивным значениям, выглядящим так же, как они сами. Например:
const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second); // trueОднако конструирование объектных значений, внешне выглядящих одинаково, не приведёт к тому, что получатся сущности, при сравнении которых будет выявлено их равенство друг другу. Проверить это можно так:
const obj1 = < name: "Intrinsic" >; const obj2 = < name: "Intrinsic" >; console.log(obj1 === obj2); // false // При этом их свойства .name являются примитивными значениями: console.log(obj1.name === obj2.name); // trueОбъекты играют фундаментальную роль в JavaScript. Они применяются буквально повсюду. Например, часто их используют в виде коллекций вида ключ/значение. Но до появления типа данных Symbol в качестве ключей объектов можно было применять лишь строки. В этом крылось серьёзное ограничение использования объектов в виде коллекций. При попытке назначения нестрокового значения в виде ключа объекта это значение приводилось к строке. Убедиться в этом можно так:
const obj = <>; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[<>] = 'someobj'; console.log(obj); //
Кстати, хотя это немного уводит нас от темы символов, хочется отметить, что структура данных Map была создана для того чтобы позволить использовать хранилища данных формата ключ/значение в ситуациях, когда ключ не является строкой.
Что такое символ?
Теперь, когда мы выяснили особенности примитивных значений в JavaScript, мы наконец готовы к тому, чтобы приступить к разговору о символах. Символ — это уникальное примитивное значение. Если подходить к символам с этой позиции, то можно заметить, что символы в этом плане похожи на объекты, так как создание нескольких экземпляров символов приведёт к созданию разных значений. Но символы, кроме того, являются иммутабельными примитивными значениями. Вот пример работы с символами:
const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // falseПри создании экземпляра символа можно воспользоваться необязательным первым строковым аргументом. Этот аргумент представляет собой описание символа, которое предназначено для использования при отладке. На сам символ это значение не влияет.
const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str); // false console.log(s1 === s2); // false console.log(s1); // Symbol(debug)Символы как ключи свойств объектов
Символы можно использовать в качестве ключей свойств объектов. Это очень важно. Вот пример использования их в таком качестве:
const obj = <>; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj); // < bar: 'bar' >console.log(sym in obj); // true console.log(obj[sym]); // foo console.log(Object.keys(obj)); // ['bar']Обратите внимание на то, что ключи, заданные символами, не возвращаются при вызове метода Object.keys() . Код, написанный до появления в JS символов, ничего о них не знает, в результате сведения о ключах объектов, представленных символами, не должны возвращаться древним методом Object.keys() .
На первый взгляд может показаться, что вышеописанные особенности символов позволяют использовать их для создания приватных свойств JS-объектов. Во многих других языках программирования можно создавать скрытые свойства объектов с использованием классов. Отсутствие этой возможности уже давно считается одним из недостатков JavaScript.
К сожалению, код, который работает с объектами, может беспрепятственно обращаться к их строковым ключам. Код может обращаться и к ключам, заданным символами, причём, даже в том случае, если у кода, из которого работают с объектом, нет доступа к соответствующему символу. Например, с помощью метода Reflect.ownKeys() можно получить список всех ключей объекта, и тех, что являются строками, и тех, что являются символами:
function tryToAddPrivate(o) < o[Symbol('Pseudo Private')] = 42; >const obj = < prop: 'hello' >; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj)); // [ 'prop', Symbol(Pseudo Private) ] console.log(obj[Reflect.ownKeys(obj)[1]]); // 42Обратите внимание на то, что в настоящее время ведётся работа над тем, чтобы оснастить классы возможностью использования приватных свойств. Эта возможность называется Private Fields (приватные поля). Она, правда, не затрагивает абсолютно все объекты, относясь лишь к тем из них, которые созданы на основе предварительно подготовленных классов. Поддержка приватных полей уже имеется в браузере Chrome версии 72 и старше.
Предотвращение коллизий имён свойств объектов
Символы, конечно, не добавляют в JavaScript возможностей по созданию приватных свойств объектов, но они являются ценным новшеством языка по другим причинам. А именно, они полезны в ситуациях, когда неким библиотекам нужно добавлять свойства в объекты, описанные за их пределами, и при этом не опасаться коллизии имён свойств объектов.
Рассмотрим пример, в котором две различные библиотеки хотят добавить к объекту метаданные. Возможно, и той и другой библиотеке нужно оснастить объект некими идентификаторами. Если для имени подобного свойства просто использовать нечто вроде строки id , состоящей из двух букв, можно столкнуться с ситуацией, когда одна библиотека перезапишет свойство, заданное другой.
function lib1tag(obj) < obj.id = 42; >function lib2tag(obj)
Если же воспользоваться в нашем примере символами, то каждая библиотека может сгенерировать, при инициализации, нужные ей символы. Затем эти символы могут быть использованы для назначения свойств объектам и для доступа к этим свойствам.
const library1property = Symbol('lib1'); function lib1tag(obj) < obj[library1property] = 42; >const library2property = Symbol('lib2'); function lib2tag(obj)Именно глядя на подобный сценарий можно ощутить пользу от появления символов в JavaScript.
Однако тут может возникнуть вопрос, касающийся использования библиотеками, для имён свойств объектов, случайных строк или строк, со сложной структурой, включающих в себя, например, название библиотеки. Подобные строки могут образовывать нечто вроде пространств имён для идентификаторов, используемых библиотеками. Например, это может выглядеть так:
const library1property = uuid(); // вызов функции для получения случайного значения function lib1tag(obj) < obj[library1property] = 42; >const library2property = 'LIB2-NAMESPACE-id'; // использование пространства имён библиотеки function lib2tag(obj)
В общем-то, можно поступить и так. Подобные подходы, на самом деле, очень похожи на то, что происходит при использовании символов. И если, используя случайные идентификаторы или пространства имён, пара библиотек не сгенерирует, по воле случая, одинаковые имена свойств, то проблем с именами не будет.
Проницательный читатель сказал бы сейчас, что два рассматриваемых подхода к именованию свойств объектов не являются полностью эквивалентными. У имён свойств, которые формируются случайным образом или с использованием пространств имён, есть недостаток: соответствующие ключи очень легко обнаружить, особенно если в коде выполняется перебор ключей объектов или их сериализация. Рассмотрим следующий пример:
const library2property = 'LIB2-NAMESPACE-id'; // используется пространство имён function lib2tag(obj) < obj[library2property] = 369; >const user = < name: 'Thomas Hunter II', age: 32 >; lib2tag(user); JSON.stringify(user); // ''Если бы в этой ситуации для имени ключа использовался бы символ, тогда JSON-представление объекта не содержало бы значения символа. Почему это так? Дело в том, что то, что в JavaScript появился новый тип данных, ещё не означает того, что изменения внесены и в спецификацию JSON. JSON поддерживает, в качестве ключей свойств объектов, только строки. При сериализации объекта не делается попыток представить символы в каком-то особом виде.
Рассматриваемую проблему попадания имён свойств в JSON-представление объектов можно решить благодаря использованию Object.defineProperty() :
const library2property = uuid(); // случайное значение function lib2tag(obj) < Object.defineProperty(obj, library2property, < enumerable: false, value: 369 >); > const user = < name: 'Thomas Hunter II', age: 32 >; lib2tag(user); // '' console.log(JSON.stringify(user)); console.log(user[library2property]); // 369Строковые ключи, «скрытые» благодаря установке их дескриптора enumerable в значение false , ведут себя практически так же, как и ключи, представленные символами. И те и другие не выводятся при вызове Object.keys() , и те и другие можно обнаружить, воспользовавшись Reflect.ownKeys() . Вот как это выглядит:
const obj = <>; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', < enumberable: false, value: 2 >); console.log(Object.keys(obj)); // [] console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ] console.log(JSON.stringify(obj)); // <>Тут, надо сказать, мы почти воссоздали возможности символов, пользуясь другими средствами JS. В частности, и ключи, представленные символами, и скрытые ключи не попадают в JSON-представление объекта. И те и другие можно узнать, обратившись к методу Reflect.ownKeys() . В результате и те и другие нельзя назвать по-настоящему приватными. Если предположить, что для формирования имён ключей используются некие случайные значения или пространства имён библиотек, то это означает, что от риска возникновения коллизии имён мы избавились.
Однако между использованием имён-символов и имён, созданных с использованием других механизмов, есть одно маленькое различие. Так как строки иммутабельны, а символы гарантированно уникальны, всегда есть возможность того, что кто-то, перебрав все возможные сочетания символов в строке, вызовет коллизию имён. С математической точки зрения это означает, что символы действительно дают нам ценную возможность, которая отсутствует у строк.
В Node.js, при исследовании объектов (например, с использованием console.log() ), если обнаруживается метод объекта, имеющий имя inspect , то, для получения строкового представления объекта и последующего его вывода на экран, используется именно этот метод. Несложно понять, что абсолютно все не могут этого учитывать, поэтому подобное поведение системы может привести к вызову метода объекта inspect , который предназначен для решения задач, не относящихся к формированию строкового представления объекта. Эта возможность признана устаревшей в Node.js 10, в 11 версии методы с подобным именем просто игнорируются. Теперь для реализации этой возможности предусмотрен символ require(‘util’).inspect.custom . А это значит, что никто уже никогда не сможет непреднамеренно нарушить работу системы, создав метод объекта с именем inspect .
Имитация приватных свойств
Вот интересный подход, который можно использовать для имитации приватных свойств объектов. Этот подход предусматривает применение ещё одной современной возможности JavaScript — прокси-объектов. Такие объекты служат обёртками для других объектов, которые позволяют программисту вмешиваться в действия, выполняемые с этими объектами.
Прокси-объекты предлагают много способов для перехвата действий, выполняемых над объектами. Нас интересует возможность управления операциями чтения ключей объекта. В подробности о прокси-объектах мы тут углубляться не будем. Если вам они интересны — взгляните на эту публикацию.
Мы можем использовать прокси для того чтобы управлять тем, какие свойства объекта видны извне. В данном случае мы хотим создать прокси, который скрывает два известных нам свойства. Одно имеет строковое имя _favColor , а второе представлено символом, записанным в переменную favBook :
let proxy; < const favBook = Symbol('fav book'); const obj = < name: 'Thomas Hunter II', age: 32, _favColor: 'blue', [favBook]: 'Metro 2033', [Symbol('visible')]: 'foo' >; const handler = < ownKeys: (target) => < const reportedKeys = []; const actualKeys = Reflect.ownKeys(target); for (const key of actualKeys) < if (key === favBook || key === '_favColor') < continue; >reportedKeys.push(key); > return reportedKeys; > >; proxy = new Proxy(obj, handler); > console.log(Object.keys(proxy)); // [ 'name', 'age' ] console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ] console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ] console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)] console.log(proxy._favColor); // 'blueСправиться со свойством, имя которого представлено строкой _favColor , несложно: достаточно почитать исходный код. Динамические ключи (наподобие uuid-ключей, которые мы видели выше), можно подобрать брутфорсом. Но без ссылки на символ получить доступ к значению Metro 2033 из объекта proxy нельзя.
Тут надо отметить, что в Node.js есть одна особенность, нарушающая приватность прокси-объектов. Эта особенность не существует в самом языке, поэтому она не актуальна для других сред выполнения JS, таких, как браузер. Дело в том, что эта особенность позволяет получать доступ к объекту, скрытому за прокси-объектом, при наличии доступа к прокси-объекту. Вот пример, демонстрирующий возможность обхода механизмов, показанных в предыдущем фрагменте кода:
const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]); // Symbol(fav book)Теперь, чтобы предотвратить использование этой особенности в конкретном экземпляре Node.js, нужно либо модифицировать глобальный объект Reflect , либо привязку процесса util . Однако это — та ещё задача. Если вам это интересно — взгляните на эту публикацию, посвящённую защите API, основанных на JavaScript.
Итоги
В этом материале мы поговорили о типе данных Symbol , о том, какие возможности он даёт JavaScript-разработчикам, и о том, какие существующие механизмы языка можно использовать для имитации этих возможностей.
Уважаемые читатели! Пользуетесь ли вы символами в своих JavaScript-проектах?
