Symbol.toPrimitive и Symbol.iterator
Symbol.toPrimitiveиSymbol.iterator— well-known символы, через которые объект кастомизирует своё поведение при приведении к примитиву (+,==, шаблонные строки) и при итерации (for...of, spread, деструктуризация).
Зачем нужно
Well-known символы — официальный механизм расширения встроенных языковых протоколов. Реализовав [Symbol.toPrimitive], объект управляет тем, как он преобразуется в число или строку. Реализовав [Symbol.iterator], объект становится итерируемым — работает с любым кодом, ожидающим iterable.
Где используется
Symbol.toPrimitive— кастомные объекты-деньги, векторы, матрицы, дата/времяSymbol.iterator— кастомные коллекции, потоки данных, генераторы- Интеграция с библиотеками, ожидающими iterable (lodash, RxJS)
- Перегрузка операторов (насколько это возможно в JS)
Основной контент
Symbol.toPrimitive
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
// hint: 'number', 'string', 'default'
switch (hint) {
case 'number':
return this.amount;
case 'string':
return `${this.amount} ${this.currency}`;
default: // используется в шаблонах и + с неизвестным типом
return this.amount;
}
}
}
const price = new Money(100, 'RUB');
// Числовой контекст
console.log(+price); // 100
console.log(price * 2); // 200
console.log(price > 50); // true
// Строковый контекст
console.log(`Цена: ${price}`); // 'Цена: 100 RUB'
console.log(String(price)); // '100 RUB'
// Дефолтный (шаблон + конкатенация)
console.log(price + ' за штуку'); // '100 за штуку'
Symbol.toPrimitive vs toString/valueOf
// До Symbol.toPrimitive: valueOf для чисел, toString для строк
class OldStyle {
constructor(n) { this.n = n; }
valueOf { return this.n; } // число
toString { return `[Value: ${this.n}]`; } // строка
}
// Symbol.toPrimitive — одно место с явным hint
class NewStyle {
constructor(n) { this.n = n; }
[Symbol.toPrimitive](hint) {
if (hint === 'string') return `[Value: ${this.n}]`;
return this.n; // number и default
}
}
Symbol.iterator (подробно)
class InfiniteCounter {
constructor(start = 0, step = 1) {
this.current = start;
this.step = step;
}
[Symbol.iterator] {
let value = this.current;
const step = this.step;
return {
next {
return { value: (value += step) - step, done: false };
},
// Необязательно, но полезно: return для досрочного завершения
return(v) {
console.log('Итерация прервана');
return { value: v, done: true };
}
};
}
}
const counter = new InfiniteCounter(0, 2);
// Берём первые 5 чётных чисел
const result = ;
for (const n of counter) {
result.push(n);
if (result.length >= 5) break; // вызовет return
}
console.log(result); // [0, 2, 4, 6, 8]
Другие well-known символы
// Symbol.hasInstance — кастомный instanceof
class EvenNumber {
static [Symbol.hasInstance](n) {
return Number.isInteger(n) && n % 2 === 0;
}
}
console.log(4 instanceof EvenNumber); // true
console.log(3 instanceof EvenNumber); // false
// Symbol.toStringTag — кастомный Object.prototype.toString()
class MyCollection {
get [Symbol.toStringTag] { return 'MyCollection'; }
}
console.log(Object.prototype.toString().call(new MyCollection));
// '[object MyCollection]'
Частые ошибки
- Возврат объекта из
[Symbol.toPrimitive]— метод должен возвращать примитив. Возврат объекта вызываетTypeError. - Игнорирование hint — если не различать
'number'/'string'/'default', поведение может быть непредсказуемым в разных контекстах. - Бесконечный итератор без защиты —
[...infiniteObj]зависнет. Для бесконечных итераторов используйте толькоfor...ofсbreakилиtake.
Связанные темы
Ресурсы
⚡ Источник: Почему в JavaScript прибавить число к объекту - это круто · AsForJS
- 📅 2023-05-18 · YouTube
- Тезисы:
ToPrimitive(obj, hint)— abstract operation, ищет[Symbol.toPrimitive]→ если нет,valueOf→toString(для hint "default"/"number") илиtoString→valueOf(для hint "string")- hint передаётся автоматически:
+obj→ "number",${obj}→ "string",obj + obj→ "default" - Symbol.toPrimitive позволяет полностью переопределить поведение объекта в operator контекстах