Exploring JavaScript Symbols. Symbol — новый тип данных в JavaScript
Это первая часть про символы и их использование в JavaScript.Новая спецификация ECMAScript (ES6) вводит дополнительный тип данных — символ (symbol). Он пополнит список уже доступных примитивных типов (string, number, boolean, null, undefined). Интересной особенностью символа по сравнению с остальными примитивными типами является то, что он единственный тип у которого нет литерала.
Для чего же нужен был дополнительный тип данных?
В JavaScript нет возможности объявить свойство объекта как приватное. Чтобы скрыть данные можно использовать замыкания, но тогда все свойства нужно объявлять в конструкторе (так как нет возможности объявить их в прототипе), к тому же они будут создаваться для каждого экземпляра, что увеличит размер используемой памяти. ECMAScript 5 предоставил возможность указать enumerable: false для свойства, что позволяет скрыть свойство от перечисления в for-in и его не будет видно в Object.keys, но для этого нужно объявлять его через конструкцию Object.defineProperty.
var user = {};
Object.defineProperty (user, 'role', { enumerable: false, value: 'admin' }); Такая конструкция объявления всё равно не лишает возможности получить значение свойства, если напрямую обратиться к нему: var userRole = user.role; // 'admin' В других языках, к примеру, можно добавить модификатор метода, чтобы определить его видимость (protected, private, public). Но в новой спецификации JavaScript выбрали другой подход и решили не вводить модификаторы, а определять поведение в зависимости от типа идентификатора свойства. Раньше имя свойства было строкой, теперь же это может быть как строка так и символ. Такой подход позволяет не менять саму концепцию объявления объектов: var role = Symbol (); var user = { id: 1001, name: 'Administrator', [role]: 'admin' }; В данном примере объявлен объект user у которого два свойства объявлены через строковые идентификаторы (id, name) и одно свойство через символ (role).Свойство role объявлено в квадратных скобках, чтобы оно не интерпретировалось как строка, а было получено в результате вычисления выражения. Данный объект можно также объявить следующим образом, чтобы лучше понять данную конструкцию: var role = Symbol (); var user = { ['id']: 1001, ['name']: 'Administrator', [role]: 'admin' }; В данном случае будут вычислены все три выражения и их результаты будут именами свойств. Возможность использовать динамические (получаемые в результате вычисления выражения) имена свойств для литералов объекта добавлены в ES6.Ключевой особенностью символа, которой он отличается от строки, является то, что обратиться к свойству которое объявлено через символ можно только по ссылке на данный символ. К примеру, eсли у объекта user нужно получить имя пользователя нужно написать данный код:
var userName = user.name; // 'Administrator' // OR var userName = user['name']; // 'Administrator' Получить роль пользователя таким образом мы не можем: var userRole = user.role; // undefined // OR var userRole = user['role']; // undefined Для того, чтобы получить роль, нужно обращаться к свойству по ссылке на символ: var role = Symbol (); var user = { id: 1001, name: 'Administrator', [role]: 'admin' };
var userRole = user[role]; // 'admin' Свойство объявленное через символ не будет видно в for-in, Object.keys, Object.getOwnPropertyNames, также не будет добавлено при использовании JSON.stringify.Рассмотрим особенности символов.
Как уже было показано в примере выше, чтобы создать символ нужно вызвать функцию Symbol:
var score = Symbol (); Функция Symbol также принимает необязательный параметр — строку, которая служит для описания символа: var score = Symbol ('user score');
console.log (score); // Symbol (user score) Описание символа служит только для того, чтобы помочь при отладке, оно не изменяет поведение символа и обратиться к символу через описание нельзя, также нет метода, чтобы получить или изменить описание символа.Спецификация ES6 больше не поддерживает явное создание объектов примитивов, поэтому следующая конструкция выбросит ошибку:
var score = new Symbol ('score'); // TypeError В целях обратной совместимости для String, Number и Boolean — ошибка не будет выбрасываться (но лучше не использовать устарешнее поведение). Если нужно работать не с примитивом, а с его объектом можно воспользоваться функцией Object передав ей примитив в качестве параметра: var symbol = Symbol ('symbol'); var string = 'string'; var number = 5;
var symbolObj = Object (symbol); var stringObj = Object (string); var numberObj = Object (number);
console.log (symbol); // Symbol (symbol) console.log (string); // 'string' console.log (number); // 5 console.log (symbolObj); // Symbol {} console.log (stringObj); // String { 0: 's', 1: 't', 2: 'r', 3: 'i', 4: 'n', 5: 'g', length: 6, [[PrimitiveValue]]: 'string' } console.log (numberObj); // Number { [[PrimitiveValue]]: 5 } Важной особенностью символа также является то, что его значение уникально: var firstScore = Symbol ('score'); var secondScore = Symbol ('score');
firstScore === secondScore; // false Это поведение открывает перед нами больше возможностей при работе с объектами, например, несколько модулей могут расширять объект новыми свойствами, не беспокоясь за возможные конфликты имен.Для определения символа можно использовать typeof, в случае если значения является символом будет возвращена строка symbol:
function isSymbol (value) { return typeof value === 'symbol'; }
var firstScore = Symbol ('score'); var secondScore = 'score';
isSymbol (firstScore); // true isSymbol (secondScore); // false В текущей системе приведения типов JavaScript есть много нюансов и символы добавляют еще одну особенность тем, что в отличии от остальных примитивных значений символ нельзя преобразовать к строке или числу. При попытке преобразовать к числу или строке будет выброшена ошибка TypeError. Такое поведение выбрано для того, чтобы случайно не создать строковое значение, которое в итоге будет использовано как имя свойства: var userObject = {}; var role = Symbol () + 'type'; var id = 10001;
userObject.id = id; userObject[ role ] = 'admin'; В данном примере не однозначно, что должно быть в результате сохранено в переменную role, если строка, тогда свойство userObject[ role ] = 'admin' будет объявлено через строку и к нему будет прямой доступ (но так как использовался символ, скорее всего было желание скрыть значение свойства). С другой стороны, если в результатом выражения будет символ, а так как получить значения символа нельзя, значит определить наличие в нем строки type нельзя, и это уже не явное поведение и нужно информировать разработчика в ситуациях, когда он преднамеренно пытается создать строковое значение из символа, потому что такая конструкция не имеет смысла.Чтобы не было такой неоднозначности, и было выбрано поведение, что при попытке преобразовать символ будет ошибка.
Это основная информация о символах, как о типе данных. В следующей части продолжим рассматривать символ и изучать методы символа (как создать глобальный символ, как работет Object.getOwnPropertySymbols), также посмотрим на возможные примеры использования символа.