javascript for impatient programmers. глава 20. символы [перевод]
Tags: javascript, books, webdev
Published: 2020-04-22 07:25:17

Вольный перевод главы книги Dr. Axel Rauschmayer "[JavaScript for impatient programmers. Symbols](https://exploringjs.com/impatient-js/ch_symbols.html)"

## 20 Символы

Символы - это примитивные значения, создаваемые с помощью функции-фабрики `Symbol()`:

    const mySymbol = Symbol('mySymbol');

Аргумент опционален и предоставляет описание, которое чаще всего используется для отладки.

С одной стороны, символы похожи на объекты, но с отличием в том, что каждое значение, созданное с помощью`Symbol()`, уникально и не может сравниваться по значению:

    > Symbol() === Symbol()
    false

С другой стороны, символы также ведут себя как примитивы и должны быть категоризированы с помощью `typeof`:

    const sym = Symbol();
    assert.equal(typeof sym, 'symbol');

И могут выступать в роли ключей для свойств объектов:

    const obj = {
      [sym]: 123,
    };

### 20.1 Случаи использования

Чаще всего символы используются в качестве:

- Значений для констант
- Уникальных ключей для свойств

### 20.1.1 Символы: значения для констант

Представим, что вы хотите создать переменных, представляющие цвета red, orange, yellow, green, blue и violet. Это можно сделать с использованием обычных строк:

    const COLOR_BLUE = 'Blue';

Плюсом такого подхода будет логгирование такой константы, поскольку оно даст очень полезный выходные данные. Минусом - есть риск ошибочного принятия несвязанного значения цвета, поскольку две строки с одинаковым содержимым рассматриваются как равные:

    const MOOD_BLUE = 'Blue';
    assert.equal(COLOR_BLUE, MOOD_BLUE);

Эту проблему можно обойти с помощью символов:

    const COLOR_BLUE = Symbol('Blue');
    const MOOD_BLUE = Symbol('Blue');
    
    assert.notEqual(COLOR_BLUE, MOOD_BLUE);

Давайте создадим функцию с помощью констант с символьным значением:

    const COLOR_RED    = Symbol('Red');
    const COLOR_ORANGE = Symbol('Orange');
    const COLOR_YELLOW = Symbol('Yellow');
    const COLOR_GREEN  = Symbol('Green');
    const COLOR_BLUE   = Symbol('Blue');
    const COLOR_VIOLET = Symbol('Violet');
    
    function getComplement(color) {
      switch (color) {
        case COLOR_RED:
          return COLOR_GREEN;
        case COLOR_ORANGE:
          return COLOR_BLUE;
        case COLOR_YELLOW:
          return COLOR_VIOLET;
        case COLOR_GREEN:
          return COLOR_RED;
        case COLOR_BLUE:
          return COLOR_ORANGE;
        case COLOR_VIOLET:
          return COLOR_YELLOW;
        default:
          throw new Exception('Unknown color: '+color);
      }
    }
    assert.equal(getComplement(COLOR_YELLOW), COLOR_VIOLET);

### 20.1.2 Символы: уникальные ключи для свойств

Ключи свойств объектов используются на двух уровнях:

- *Базовый уровень*. Ключи отражают проблему, которую решает программа.
- *Meta уровень*. Ключи используются сервисами (библиотеками и ECMAScript), работающими с данными и кодом на базовом уровне. Одним из таких ключей является `'toString'`.

Код ниже показывает разницу между ними:

    const pt = {
      x: 7,
      y: 4,
      toString() {
        return `(${this.x}, ${this.y})`;
      },
    };
    assert.equal(String(pt), '(7, 4)');

Свойства `.x` и `.y` существуют на базовом уровне. Они хранят координаты точки, представленной `pt` и используются для решения проблемы – вычисления с точками. Метод `.toString()` существует на meta уровне. Используется JavaScript для конвертации этого объекта в строку.

Свойства meta уровня никогда не должны смешиваться со свойствами базового. Это значит, что их ключи никогда не должны перекрывать друг друга. Этому условию бывает тяжело следовать, если и язык, и библиотеки работают с meta уровнем. Например, сейчас невозможно задавать новым методам meta уровня простые имена, вроде `toString`, поскольку они могут конфликтовать с уже существующими именами базового уровня. В Python эта проблема была решена путем добавления префикса и суффикса в виде двух подчеркиваний к специальным именам: `__init__`, `__iter__`, `__hash__` и т.д. .Однако, даже с таким решением, библиотеки не могут иметь собственные свойства на meta уровне, потому что есть вероятность конфликта с будущими свойствами языка.

Символы, используемые в качестве ключей для свойств объекта, могут помочь здесь: каждый символ уникален, и никогда не вступит в конфликт с любой другой строкой или символьным ключом.

### 20.1.2.1 Пример: библиотека с методом на meta уровне

В качестве примера, давайте представим библиотеку, которая по-разному обрабатывает объекты, если в них реализован специальный метод. Определение ключа и реализация такого метода выглядела бы следующим образом:

    const specialMethod = Symbol('specialMethod');
    const obj = {
      _id: 'kf12oi',
      [specialMethod]() { // (A)
        return this._id;
      }
    };
    assert.equal(obj[specialMethod](), 'kf12oi');

Квадратные скобки в строке A позволяют нам указать, что метод должен иметь ключ `specialMethod`. Больше подробностей можно найти в [§25.5.2 “Computed property keys”](https://exploringjs.com/impatient-js/ch_single-objects.html#computed-property-keys).

### 20.2 Известные символы

Играющие специальные роли символы, встроенные в ECMAScript, называются *известными символами*. Примеры включают:

- `Symbol.iterator`: делает объект *итерируемым*. Это ключ метода, который возвращает итератор. Больше подробностей можно найти в [§27 “Synchronous iteration”](https://exploringjs.com/impatient-js/ch_sync-iteration.html).
- `Symbol.hasInstance`: кастомизирует работу `instanceof`. Если объект реализует метод с таким ключом, то его можно использовать в правой части этого оператора. Пример:

        const PrimitiveNull = {
          [Symbol.hasInstance](x) {
            return x === null;
          }
        };
        assert.equal(null instanceof PrimitiveNull, true);

- `Symbol.toStringTag`: влияет на дефолтный метод `.toString()`.

        > String({})
        '[object Object]'
        > String({ [Symbol.toStringTag]: 'is no money' })
        '[object is no money]'

    Примечание:  `.toString()`, обычно, лучше переопределять.

### 20.3 Конвертация символов

Что произойдет, если мы конвертируем символ `sym` в другой примитивный тип? В таблице приведены ответы.

| Convert to | Explicit conversion     | Coercion (implicit conv.) |
|------------|-------------------------|---------------------------|
| boolean    | Boolean(sym) → OK       | !sym → OK                 |
| number     | Number(sym) → TypeError | sym*2 → TypeError         |
| string     | String(sym) → OK        | ''+sym → TypeError        |
|            | sym.toString() → OK     | `${sym}` → TypeError      |

Ключевая ловушка символов - как часто выбрасываются исключения, когда мы конвертируем их во что-то другое. Что за этим стоит? Во-первых, преобразование к числу вообще не имеет смысла, о чем должно быть предупреждение. Во-вторых, преобразование к строке действительно полезно для оценки выходных данных. Но предупреждать случайные конвертации в строку (которая является совершенно другим типом ключа) тоже имеет смысл:

    const obj = {};
    const sym = Symbol();
    assert.throws(
      () => { obj['__'+sym+'__'] = true },
      { message: 'Cannot convert a Symbol value to a string' });

Недостатком является то, что исключения делают работу с символами более сложной. Вам приходится явно преобразовывать символы при конкатенации строк:

    > const mySymbol = Symbol('mySymbol');
    > 'Symbol I used: ' + mySymbol
    TypeError: Cannot convert a Symbol value to a string
    > 'Symbol I used: ' + String(mySymbol)
    'Symbol I used: Symbol(mySymbol)'