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] → если нет, valueOftoString (для hint "default"/"number") или toStringvalueOf (для hint "string")
    • hint передаётся автоматически: +obj → "number", ${obj} → "string", obj + obj → "default"
    • Symbol.toPrimitive позволяет полностью переопределить поведение объекта в operator контекстах