javascript for impatient programmers. глава 20. символы [перевод]
Вольный перевод главы книги 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 = {
[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”.
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)'