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

ссылка на оригинал статьи http://habrahabr.ru/post/255137/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *