← JavaScript/Преобразование объектов в примитивы#112 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Преобразование объектов в примитивы

Ты пишешь класс Money для финансового приложения. Хочется, чтобы price + 500 вернуло число, а `Цена: ${price} вернуло строку "2500 ₽". JavaScript позволяет это через механизм Symbol.toPrimitive` — одну точку управления всеми видами преобразований.

На основе предыдущих уроков

  • «Типы данных» — примитивы: number, string, boolean
  • «Преобразование типов» — явное и неявное преобразование, Number(), String()
  • «Классы» — синтаксис class, конструктор, методы
  • «Геттеры/сеттеры» — get/set в классах
  • Когда происходит конвертация

    const obj = { value: 42 }
    
    obj + 10          // арифметика → числовой хинт
    `Цена: ${obj}`  // шаблонная строка → строковый хинт
    if (obj) { }     // логический контекст → всегда true (объекты truthy)

    Хинты (hints)

    JS передаёт в метод конвертации один из трёх хинтов:

  • "number" — числовой контекст: унарный +, арифметика, Math.sqrt(obj)
  • "string" — строковый контекст: шаблоны ${obj}, String(obj), alert(obj)
  • "default" — неоднозначный контекст: бинарный +, == с числом
  • Symbol.toPrimitive — главный метод

    Symbol.toPrimitive — встроенный символ. Если он определён на объекте, JS вызывает его при любой конвертации:

    const money = {
      amount: 1500,
      currency: 'RUB',
    
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return `${this.amount} ${this.currency}`
        if (hint === 'number') return this.amount
        return this.amount  // 'default' — ведём себя как число
      }
    }
    
    console.log(`Цена: ${money}`)  // 'Цена: 1500 RUB'  (hint: 'string')
    console.log(money + 500)         // 2000               (hint: 'default')
    console.log(+money)              // 1500               (hint: 'number')

    toString() и valueOf() как fallback

    Если Symbol.toPrimitive не определён, JS пробует:

    1. Для хинта "string": сначала toString(), потом valueOf()

    2. Для хинтов "number" и "default": сначала valueOf(), потом toString()

    class Point {
      constructor(x, y) {
        this.x = x
        this.y = y
      }
    
      toString() {
        return `Point(${this.x}, ${this.y})`
      }
    
      valueOf() {
        return Math.sqrt(this.x ** 2 + this.y ** 2)  // расстояние от начала координат
      }
    }
    
    const p = new Point(3, 4)
    console.log(String(p))    // 'Point(3, 4)'   → toString()
    console.log(p + 0)        // 5               → valueOf()
    console.log(`${p}`)      // 'Point(3, 4)'   → toString()

    Порядок применения правил

    | Хинт | Пробуется сначала | Потом |

    |------|------------------|-------|

    | "string" | toString() | valueOf() |

    | "number" | valueOf() | toString() |

    | "default" | valueOf() | toString() |

    Если Symbol.toPrimitive есть — все остальные игнорируются.

    Типичные ошибки

    1. Забыть вернуть значение из Symbol.toPrimitive — получишь TypeError:

    // Плохо: нет return для хинта 'number'
    const bad = {
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return 'текст'
        // забыли вернуть число!
      }
    }
    console.log(+bad)  // NaN — возвращает undefined, который превращается в NaN
    
    // Хорошо: обрабатываем все хинты
    const good = {
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return 'текст'
        return 42  // number и default
      }
    }
    console.log(+good)  // 42

    2. valueOf возвращает объект — JS не сможет преобразовать:

    // Плохо: valueOf возвращает объект
    class Bad {
      valueOf() { return this }  // ошибка! JS ожидает примитив
    }
    
    const b = new Bad()
    console.log(b + 1)  // '[object Object]1' — JS пошёл в toString()
    
    // Хорошо: valueOf возвращает примитив
    class Good {
      constructor(n) { this.n = n }
      valueOf() { return this.n }
    }
    console.log(new Good(5) + 1)  // 6

    3. Шаблонная строка использует хинт "string", а не "default":

    const obj = {
      [Symbol.toPrimitive](hint) {
        if (hint === 'default') return 'дефолт'
        if (hint === 'string') return 'строка'
        return 0
      }
    }
    
    console.log(`${obj}`)   // 'строка'  — хинт 'string'
    console.log(obj + '')  // 'дефолт'  — хинт 'default' (бинарный +)

    В реальных проектах

  • Финансовые классы — Money, Price, Currency: арифметика через valueOf, форматирование через toString
  • Геометрия — Vector, Point: длина/модуль через valueOf
  • Единицы измерения — Duration, Temperature: сравнение и вычитание без ручного преобразования
  • ORM / ActiveRecord — объект записи как число (primary key) или строка (toString для логов)
  • Примеры

    Класс Money с Symbol.toPrimitive для арифметики и форматированного строкового вывода

    class Money {
      constructor(amount, currency = 'RUB') {
        this.amount = amount
        this.currency = currency
      }
    
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
          return new Intl.NumberFormat('ru-RU', {
            style: 'currency',
            currency: this.currency,
            maximumFractionDigits: 0,
          }).format(this.amount)
        }
        // 'number' и 'default' → числовое значение
        return this.amount
      }
    
      add(other) {
        const otherAmount = other instanceof Money ? other.amount : other
        return new Money(this.amount + otherAmount, this.currency)
      }
    }
    
    const price = new Money(2500)
    const shipping = new Money(350)
    
    console.log(`Товар: ${price}`)        // 'Товар: 2 500 ₽'
    console.log(`Доставка: ${shipping}`)  // 'Доставка: 350 ₽'
    
    const total = price.add(shipping)
    console.log(`Итого: ${total}`)        // 'Итого: 2 850 ₽'
    
    // Арифметика через Symbol.toPrimitive с hint 'default'
    console.log(price + 100)             // 2600 (число)
    console.log(price > 1000)            // true
    console.log(+shipping)               // 350
    
    // Класс Temperature
    class Temperature {
      constructor(celsius) { this.celsius = celsius }
    
      get fahrenheit() { return this.celsius * 9 / 5 + 32 }
    
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return `${this.celsius}°C (${this.fahrenheit.toFixed(1)}°F)`
        return this.celsius
      }
    }
    
    const bodyTemp = new Temperature(36.6)
    console.log(`Температура тела: ${bodyTemp}`)  // '36.6°C (97.9°F)'
    console.log(bodyTemp > 37)                      // false — нет лихорадки
    console.log(bodyTemp + 1.5)                     // 38.1

    Преобразование объектов в примитивы

    Ты пишешь класс Money для финансового приложения. Хочется, чтобы price + 500 вернуло число, а `Цена: ${price} вернуло строку "2500 ₽". JavaScript позволяет это через механизм Symbol.toPrimitive` — одну точку управления всеми видами преобразований.

    На основе предыдущих уроков

  • «Типы данных» — примитивы: number, string, boolean
  • «Преобразование типов» — явное и неявное преобразование, Number(), String()
  • «Классы» — синтаксис class, конструктор, методы
  • «Геттеры/сеттеры» — get/set в классах
  • Когда происходит конвертация

    const obj = { value: 42 }
    
    obj + 10          // арифметика → числовой хинт
    `Цена: ${obj}`  // шаблонная строка → строковый хинт
    if (obj) { }     // логический контекст → всегда true (объекты truthy)

    Хинты (hints)

    JS передаёт в метод конвертации один из трёх хинтов:

  • "number" — числовой контекст: унарный +, арифметика, Math.sqrt(obj)
  • "string" — строковый контекст: шаблоны ${obj}, String(obj), alert(obj)
  • "default" — неоднозначный контекст: бинарный +, == с числом
  • Symbol.toPrimitive — главный метод

    Symbol.toPrimitive — встроенный символ. Если он определён на объекте, JS вызывает его при любой конвертации:

    const money = {
      amount: 1500,
      currency: 'RUB',
    
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return `${this.amount} ${this.currency}`
        if (hint === 'number') return this.amount
        return this.amount  // 'default' — ведём себя как число
      }
    }
    
    console.log(`Цена: ${money}`)  // 'Цена: 1500 RUB'  (hint: 'string')
    console.log(money + 500)         // 2000               (hint: 'default')
    console.log(+money)              // 1500               (hint: 'number')

    toString() и valueOf() как fallback

    Если Symbol.toPrimitive не определён, JS пробует:

    1. Для хинта "string": сначала toString(), потом valueOf()

    2. Для хинтов "number" и "default": сначала valueOf(), потом toString()

    class Point {
      constructor(x, y) {
        this.x = x
        this.y = y
      }
    
      toString() {
        return `Point(${this.x}, ${this.y})`
      }
    
      valueOf() {
        return Math.sqrt(this.x ** 2 + this.y ** 2)  // расстояние от начала координат
      }
    }
    
    const p = new Point(3, 4)
    console.log(String(p))    // 'Point(3, 4)'   → toString()
    console.log(p + 0)        // 5               → valueOf()
    console.log(`${p}`)      // 'Point(3, 4)'   → toString()

    Порядок применения правил

    | Хинт | Пробуется сначала | Потом |

    |------|------------------|-------|

    | "string" | toString() | valueOf() |

    | "number" | valueOf() | toString() |

    | "default" | valueOf() | toString() |

    Если Symbol.toPrimitive есть — все остальные игнорируются.

    Типичные ошибки

    1. Забыть вернуть значение из Symbol.toPrimitive — получишь TypeError:

    // Плохо: нет return для хинта 'number'
    const bad = {
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return 'текст'
        // забыли вернуть число!
      }
    }
    console.log(+bad)  // NaN — возвращает undefined, который превращается в NaN
    
    // Хорошо: обрабатываем все хинты
    const good = {
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return 'текст'
        return 42  // number и default
      }
    }
    console.log(+good)  // 42

    2. valueOf возвращает объект — JS не сможет преобразовать:

    // Плохо: valueOf возвращает объект
    class Bad {
      valueOf() { return this }  // ошибка! JS ожидает примитив
    }
    
    const b = new Bad()
    console.log(b + 1)  // '[object Object]1' — JS пошёл в toString()
    
    // Хорошо: valueOf возвращает примитив
    class Good {
      constructor(n) { this.n = n }
      valueOf() { return this.n }
    }
    console.log(new Good(5) + 1)  // 6

    3. Шаблонная строка использует хинт "string", а не "default":

    const obj = {
      [Symbol.toPrimitive](hint) {
        if (hint === 'default') return 'дефолт'
        if (hint === 'string') return 'строка'
        return 0
      }
    }
    
    console.log(`${obj}`)   // 'строка'  — хинт 'string'
    console.log(obj + '')  // 'дефолт'  — хинт 'default' (бинарный +)

    В реальных проектах

  • Финансовые классы — Money, Price, Currency: арифметика через valueOf, форматирование через toString
  • Геометрия — Vector, Point: длина/модуль через valueOf
  • Единицы измерения — Duration, Temperature: сравнение и вычитание без ручного преобразования
  • ORM / ActiveRecord — объект записи как число (primary key) или строка (toString для логов)
  • Примеры

    Класс Money с Symbol.toPrimitive для арифметики и форматированного строкового вывода

    class Money {
      constructor(amount, currency = 'RUB') {
        this.amount = amount
        this.currency = currency
      }
    
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
          return new Intl.NumberFormat('ru-RU', {
            style: 'currency',
            currency: this.currency,
            maximumFractionDigits: 0,
          }).format(this.amount)
        }
        // 'number' и 'default' → числовое значение
        return this.amount
      }
    
      add(other) {
        const otherAmount = other instanceof Money ? other.amount : other
        return new Money(this.amount + otherAmount, this.currency)
      }
    }
    
    const price = new Money(2500)
    const shipping = new Money(350)
    
    console.log(`Товар: ${price}`)        // 'Товар: 2 500 ₽'
    console.log(`Доставка: ${shipping}`)  // 'Доставка: 350 ₽'
    
    const total = price.add(shipping)
    console.log(`Итого: ${total}`)        // 'Итого: 2 850 ₽'
    
    // Арифметика через Symbol.toPrimitive с hint 'default'
    console.log(price + 100)             // 2600 (число)
    console.log(price > 1000)            // true
    console.log(+shipping)               // 350
    
    // Класс Temperature
    class Temperature {
      constructor(celsius) { this.celsius = celsius }
    
      get fahrenheit() { return this.celsius * 9 / 5 + 32 }
    
      [Symbol.toPrimitive](hint) {
        if (hint === 'string') return `${this.celsius}°C (${this.fahrenheit.toFixed(1)}°F)`
        return this.celsius
      }
    }
    
    const bodyTemp = new Temperature(36.6)
    console.log(`Температура тела: ${bodyTemp}`)  // '36.6°C (97.9°F)'
    console.log(bodyTemp > 37)                      // false — нет лихорадки
    console.log(bodyTemp + 1.5)                     // 38.1

    Задание

    В навигационном приложении используется класс Vector2D. Добавь Symbol.toPrimitive так, чтобы: при строковом хинте возвращалась строка вида "(x, y)", а при числовом и default хинте — длина вектора (Math.sqrt(x²+y²)).

    Подсказка

    hint === "string" → вернуть `(${this.x}, ${this.y})`. Иначе — Math.sqrt(this.x ** 2 + this.y ** 2). Шаблонная строка `${v}` использует хинт "string", а v + 0 — хинт "default".

    Загружаем среду выполнения...
    Загружаем AI-помощника...