arka triymfalnaya

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 Символы: уникальные ключи для свойств

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

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

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, называются известными символами. Примеры включают:

const PrimitiveNull = {
	[Symbol.hasInstance](x) {
		return x === null;
	},
};
assert.equal(null instanceof PrimitiveNull, true);
        > 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)'