Вольный перевод главы книги Dr. Axel Rauschmayer "JavaScript for impatient programmers. Symbols"
20 Символы
Символы - это примитивные значения, создаваемые с помощью функции-фабрики Symbol()
:
const mySymbol = Symbol('mySymbol');
Аргумент опционален и предоставляет описание, которое чаще всего используется для отладки.
С одной стороны, символы похожи на объекты, но с отличием в том, что каждое значение, созданное с помощьюSymbol()
, уникально и не может сравниваться по значению:
> Symbol() === Symbol()
false
С другой стороны, символы также ведут себя как примитивы и должны быть категоризированы с помощью typeof
:
const sym = Symbol();
assert.equal(typeof sym, 'symbol');
И могут выступать в роли ключей для свойств объектов:
const obj = {
};
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”.
20.2 Известные символы
Играющие специальные роли символы, встроенные в ECMAScript, называются известными символами. Примеры включают:
-
Symbol.iterator
: делает объект итерируемым. Это ключ метода, который возвращает итератор. Больше подробностей можно найти в §27 “Synchronous iteration”. -
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)'
Top comments (0)