Pull to refresh

Exploring JavaScript Symbols. Symbol — новый тип данных в JavaScript

Reading time5 min
Views32K
Это первая часть про символы и их использование в 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), также посмотрим на возможные примеры использования символа.
Tags:
Hubs:
+38
Comments61

Articles

Change theme settings