Это первая часть про символы и их использование в 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
), также посмотрим на возможные примеры использования символа.
Автор: rise2semi