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

  • Tutorial
Это первая часть про символы и их использование в 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), также посмотрим на возможные примеры использования символа.
Поделиться публикацией

Похожие публикации

Комментарии 61

    +32
    Мне одному кажется, что этот Symbol огромный костыль? Какой-то очень нетривиальный тип данных, имеющий странное название, при использовании которого есть много исключений из правил.
      0
      Нет. Читаю и думаю «Что за...» с фейспалмом за изготове.
        +6
        Да нафик не нужны эти приватные члены в javascript. Используйте просто нотацию имён, с _ — private, без — всё остальное.
        А вот динамически вычисляемые поля в json это круто. Интересно если результат вычисления null или undefined, то поле будет добавлено в объект?
          +6
          Полагаю, поведение будет таким же, как и при
          a[null] = 1; a[undefined] = 2;
          .

          А приватность нужна разработчикам библиотек. А то завяжется кто-то на приватное свойство — ему показалось так удобнее, затем этот костыль попадет в известные «фичи», его начнут рекомендовать и использовать. В итоге приватное свойство уже не приватное и его нельзя изменить.
            +3
            Клиент всегда прав, и если много программистов начинают использовать приватное свойство в своих целях — значит создатель библиотеки ошибся в архитектуре и нужно было сделать это свойство публичным, или предусмотреть другой способ выполнени того действия, которого хотят таким образом добиться. Иначе люди будут менять исходный код (иногда и мне приходится) и всё равно добиваться нужного функционала. Если я подключаю библиотеку — я надеюсь, что она будет удовлетворять моим требованиям, и не надо ничего запрещать. Оповестил, что приватное, спасибо, действую на свой страх и риск.
              –2
              Ох, сколько раз я ругался на разработчиков библиотек всяких там .Net, из-за того, что они чего-то там не додумали в публичном интерфейсе. Теперь это будет и в JS?
              0
              По существу с Вами согласен. Но в принципе думаю: пусть будет. Приватные члены, конечно не нужны, но какое-нибудь применение ему найдётся.
                0
                Это всё же не JSON.
                +1
                В других языках этот тип тоже встречается.
                Если мне не изменяет память, схожей (или даже такой же) семантикой обладают атомы в Prolog'е или символы в Ruby.
                  0
                  Насколько мне известно, символы в Ruby — один из вариантов представления строки и числа: при создании символа строка навсегда (пока работает интерпретатор) связывается с числом и :foo в разных модулях есть один и тот же символ.

                  Впрочем, такое поведение приводит к уязвимости типа DoS в определённых случаях и вроде бы «навсегда» уже не такое «навсегда».

                  Символы здесь можно сравнить с типом object в Python (если забыть, что это родитель всех (Python 3) или почти всех остальных классов): написав foo = object(), единственное, что вы можете сделать с foo полезного есть использование его в качестве гарантированно уникального объекта, который можно получить только через уже существующую ссылку. Удобно, если вы хотите что-то (де)сериализовывать и нуждаетесь в уникальном специальном значении типа True, False и None: свой класс здесь ещё не нужен, но другие примитивные типы использовать уже нельзя (они могут быть десериализованы из представления значения примитивного типа). Больше я нигде и низачем такое не использовал: в Python всё равно что-то скрыть можно, только написав это что-то на C/…
                    +1
                    Нет, семантика скорее обратная. В Ruby гарантируется, что
                    :foo == :foo # => true
                    'foo'.to_sym == :foo # => true
                    a = :foo
                    b = :foo
                    a == b # => true
                    

                    Это просто что-то вроде констант всеобщего назначения с хорошей производительностью сравнения (потому что внутри это числа).
                      0
                      И действительно…
                    0
                    Полезная штука на самом деле, если бы появилась в языке с самого начала, можно было бы обходиться без костылей типа __proto__ в объекте.
                      +3
                      Без __proto__ можно было бы обойтись и без символов, если бы изначально были добавлены Object.(get|set)PrototypeOf. Если бы символы появились в языке с самого начала, это бы избавило от костылей типа toString и прочих методов Object.prototype, ломающих словари. А теперь — поздно пить боржоми =\
                      0
                      На самом деле это очень старая идея — в CLOS используется похожая схема для инкапсуляции.
                        0
                        Symbol это не костыль, а весьма мощный подход для создания примесей. Символы позволяют примешивать в объект методы и связанные с ними состояния, при этом не оголяя состояния. Если символов нет, в худшем случае придётся вести отдельную мапу, в которой будут храниться некоторые искуственные id для объектов и связанные с ними значения стейста примеси (и поддерживать актуальность мапы), в более лучшем варианте WeakMap, где вместо ключа уже выступает сам объект. Но символ решает эту задачу чище, позволяя хранить стейт прямо в объекте и при этом не засоряя никакие ключи (коллизий нет в принципе). Также символ автоматически решает задачу освобождения памяти — как только объект с примесью уничтожается, состояния примесей также уничтожаются, потому что они хранятся в объекте.

                        Символы (вместе с WeakMap-ами) позволят лучше организовать вещи, наподобие jQuery.data, разрешат некоторые ситуации с ивентами а также будут отличным подспорьем в FRP, помогая решить ситуации с текущей памятью. Символы решат много интересных задач в библиотечном коде. В коде приложения символы же можно использовать как простой enum, например. Уверен, найдутся и другие применения.

                        И да, в отличие от многих других фич нового стандарта (от которого я далеко не в восторге), Symbol не является сахаром. 80% остальных нововведений это либо сахар, либо вещи, которые уже давно прекрасно реализуются библиотеками.
                        +1
                        мне кажется, или это то же самое, что и unique_id()?
                          0
                          ну почти, unique_id можно вывести.
                          +6
                          Рискну сказать: может быть, пора уже признать, что Javascript был с самого начала спроектирован в спешке и для других целей, и в 21-ом веке может быть можно уже заменить его какой-нибудь более подходящей для современной разработки альтернативой, в которой не надо городить костыли?
                            –2
                            Согласен с вами, а по поводу статьи — какие-то такие чувства:
                            image
                              0
                              По мне, так лучше бы оставили затею сделать из JS серьёзный интструмент и запилили бы что-то TypeScript/Dart-подобное, нативное на новой виртуальной машине.
                                –3
                                Да, давно уже пора переходить на Python.
                                  0
                                  Переходите :) pyjs.org
                                    0
                                    Мне больше понравился Brython. Но комьюнити практически нет никакого, автору явно не хватает помощи в развитии проекта.
                                    0
                                    Питон пока слабо поддерживается браузерами
                                      0
                                      JS тоже когда-то слабо поддерживался браузерами. Ничего, вот станут процессоры еще раз в 50 мощнее — и тогда станет нормой запускать интерпритаторы под интерпритаторами :) Тогда и поддержка браузерами других языков не заставит себя долго ждать.
                                  –5
                                  Ну здрасте!

                                      var role = Symbol();
                                      var user = {
                                          id: 1001,
                                          name: 'Administrator',
                                          [role]: 'admin'
                                      };
                                  
                                  user.role; 
                                  // -> undefined
                                  user.role = 'User';
                                  

                                  Что будет если так? У user два свойства role одно публичное, одно приватное? Если да, то это опять какое-то недо-ООП.
                                    +4
                                    Да, у объека user будет два свойства:

                                        Object { id: 1001, name: 'Administrator', role: 'User', Symbol(): 'admin' }
                                    

                                    А что именно здесь нелогично (ты ведь сам создаешь одно публичное и одно приватное свойство role), почему это подпадает под недо-ООП?
                                      –3
                                      Это почва для огромного количества потенциальных ошибок. Кроме того, это семантически неверно. Такое не прокатит ни в PHP, ни в Java, например.
                                        +5
                                        Это прокатит абсолютно в любом языке, где атрибуты можно брать по именам, взятым из переменной. Воспринимайте это так, как будто вы назвали атрибут a56d843e_dd66_11e4_be1e_50465d597777, записали эту строку в переменную и сказали, что будете обращаться исключительно через неё.
                                      +1
                                      Здесь своя семантика. У вас не два свойства role, у вас одно role, а другое — объект Symbol по адресу 0xNNNNNNNN. Назвать его затем можно как угодно: название переменной к самому объекту гвоздями не прибито. То, что программист библиотеки выбрал совпадающие имена — это личная проблема программиста. Он раньше точно так же мог использовать role и _role. При некоторых условиях такое даже имеет смысл: в Python так обычно реализуют «ленивые» атрибуты классов, если их в проекте всего несколько.

                                      Может вы хотите в публичном свойстве хранить объект RoleProxy с ограниченными возможностями, а в приватном — полноценный Role.
                                      +3
                                      Еще хотел добавить, что понятие типа Symbol уже имеет свою семантику в языках программирования. Гомоиконные языки имеют тип symbol, а в JS это вводит только непонятную неразбериху. Увидел заголовок, подумал, что этот тип данных как-то будет связан со структурами данных именно самого языка и будет представлять собой именно эти структуры (как символы в Лиспе, например). Но вот уж чего точно не ожидал, так то что это тип для приватных свойст. Короче говоря, как-то это нелогично очень.
                                        0
                                        Да, зря они это дело назвали символом, поскольку это прямо противоположная вещь символам и атомам из других языков. Путаница будет дичайшая.
                                        0
                                        Возможно глупый вопрос, но почему нельзя в таком случае вместо ключа использовать просто Object(), зачем нужен особый тип?
                                          0
                                          Если попытаться использовать Object() в качестве ключа, он будет приведён к строке [object Object].
                                            0
                                            Обязательное приведение ключей словаря к строке — это какая-то детская болезнь для языков, из которой JS и PHP никак не вырастут.
                                              +1
                                              Это ваше мнение, я с ним не согласен. Для словарей с объектами в качестве ключей есть Map и WeakMap.
                                                0
                                                Когда они будут поддерживаться большинством браузеров, тогда и можно будет сказать, что выросли. Пока что поддержка на мобильных устройствах оставляет надежды на будущее.
                                                  0
                                                  Они поддерживаются всеми актуальными браузерами, а для устаревших всегда есть полифилы.
                                                0
                                                Строго говоря, можно добиться этого поведения, реализовав интерфейс ArrayAccess у объекта и творить что-то вроде:
                                                $m = new Map();
                                                $obj = new Object();
                                                $m[$obj] = "woo";
                                                


                                                Но от невозможности работы с ключами отличными от целочисленного или строки во встроенных массивах это не избавит.
                                                  0
                                                  В PHP есть еще spl_object_hash(), но это все равно костыли.
                                            +3
                                            Откровенно поверхностная статья.

                                            Чего в статье нет:

                                            — Ни малейшего упоминания об Iterator protocol
                                            — Разбора типов символов
                                            Object.getOwnPropertySymbols

                                              +1
                                              Все остальные аспекты будут рассмотрены в последующих частях (я ведь это отметил в конце статьи). Вся информация будет поделена на логические части, в этой была затронута только тема символов как тип данных.
                                                0
                                                Ну Iterator protocol с символами связан только тем, что использует well-known символ Symbol.iterator. Множество другой логики ES6+ использует well-known символы — toStringTag, species и т. д.

                                                Статья — да, поверхностная, но посмотрите на последний абзац — явно указано, что это только начало.
                                                0
                                                Подскажите, а в чем приватность метода, если я все же могу получить значение, используя symbol. Или я чего-то не понял?
                                                Попадёт ли свойство, объявленное через symbol к ребёнку при наследовании?
                                                  0
                                                  Символы не приватные ключи, а уникальные. Их предназначение — предотвращение перекрытия свойств в пользовательском коде. Символы также можно получить через Object.getOwnPropertySymbols и Reflect.ownKeys. Свойство по ключу-символу наследуется точно так же, как и обычное свойство. Для приватных данных сейчас можно использовать WeakMap, хотя и они не являются полностью приватными (можно перехватить скрытые данные, обернув методы прототипа коллекции). Для хранения приватных данных, в ES7+ планируется добавить приватные поля — эта концепция, к сожалению, заменила более удачную, с моей точки зрения, концепцию абстрактных ссылок.
                                                  +6
                                                  В статье этот концепт был подан как приватные свойства, но если сделать основной акцент на то, что это способ создавать уникальные свойства (как указано в комментарии выше), то мне уже не кажется это костылем, а, наоборот, вполне себе фича. Тем более, что есть даже метод Symbol.for(), который позволяет хранить символы глобально для «среды текущего выполнения». То есть их приватность — это все также соглашение, а не закон.

                                                  Вот, а реальные приватные члены объекта в любом случае потянут за собой лексические изменения (введение модификаторов), т.к. привязку к лексическому окружению проверяет интерпретатор. Причем, кстати, введение модификаторов не лишает свободы, ведь их можно не использовать.

                                                  Какая свобода потенциально возможна: какие-то члены в виде символов, какие-то забиты в WeakMap, какие-то в виде обычных замыканий, какие-то в виде _ (немного приватные), какие-то в виде __ (чуть больше приватные), и тут еще будут модификаторы в будущих спецификациях (настоящие приватные). А еще говорят, что JS — это «детский язык». Да более мощного механизма приватных членов нигде нет :)
                                                    –2
                                                    Автор, я умоляю тебя обратить внимание на одну важную часть: черновик ES6! =) Во всём посту и в комментариях ни слова о сей существенной детали, говорящей, что всё это пока не стандарт и может быть изменено.

                                                    Вы эпизодически говорите «спецификация ECMAScript». Но такой сущности нет. Есть только черновик спецификации ECMAScript.
                                                      +4
                                                      Ничего уже не может быть изменено. Текущая версия — RC4 — финальная и именно она будет утверждаться ECMA в июне.
                                                        0
                                                        Спасибо за линк. Стоило это упомянуть в статье.

                                                        минусить вы в праве, но...
                                                        Официальный статус на момент написания — черновик. Где не прав?
                                                        Деталь существенная. Не хотел бы я жить в здании построенном на техническом черновике, а не утверждённом по всем протоколам чертежам. Где не прав?
                                                        Возможно твит правда, кто в ответе если нет? (я надуюсь, что это так, но рассчитывать на это в серьёзных приложениях стоит ли?.. Серьёзных = материальная ответственность)
                                                          0
                                                          Не хотел бы я жить в здании построенном на техническом черновике, а не утверждённом по всем протоколам чертежам.


                                                          Можно подумать, будто таких зданий не строят, пообщайтесь с инженерами-конструкторами :)
                                                            +1
                                                            С несколько десятков многоэтажек стоит со статусом «самострой». А ещё людей сбивают машины, их убивают микроорганизмы. Наличие явление это одно, а желание (или нежелание) быть к нему причастным — другое)

                                                            Подумать можно всегда.., только WAT? при чём тут мои слова?) Это как «я не хотел бы быть сбитым машиной» и трактовка «можно подумать, будто людей не сбивают машины. Пообщайтесь с ДПС» =)
                                                        0
                                                        Как уже ответили выше, можно не переживать, что это черновик, так как все уже утверждено и войдёт в спецификацию. Последнюю версию (Release Candidate 4 от 3 апреля) можно найти — здесь.
                                                          0
                                                          Если вас связали боровы и везут в лес — тоже можно не переживать =)
                                                          Как написал выше, этими сведениями не стоило преминуть. Для ясности.
                                                        0
                                                        Кстати. А что произойдет, если объект с ключом типа Symbol прогнать через JSON.stringify? Он никак не отобразится при сериализации?
                                                          0
                                                          Да, будет проигнорирован
                                                          +3
                                                          Братишки, я вам почитать принёс: es6-features.org
                                                            +1
                                                            Ещё в копилочку (хотя думаю многие видели): kangax.github.io/compat-table/es6/
                                                              0
                                                              Я не видел, но когда посмотрел, испугался. Цитируя один мультфильм, скажу: «Мы впервые столкнулись с человеком, у которого совершенно нет личной жизни».
                                                                0
                                                                Не знаю его, но он жёсткий тип. Проделана колоссальная работа. И не только по es6.
                                                              +1
                                                              Занятная ссылочка. Захожу, начинаю читать, само собой, снизу. Reflect. ES6 пример — копипаста моего из документации core-js, что позже скопировал в документацию babel. ES5 пример сломан, пишу issue — исправляют. Двигаюсь дальше, следующий пример — Proxy, тоже сломан. Пишу issue — автор проблемы не видит. Дальше читать уже боюсь.

                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                            Самое читаемое