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

Symbol

В Node.js библиотека http добавляет на объект запроса служебные свойства — но не хочет конфликтовать с полями которые создаёт разработчик. В React у каждого JSX-элемента есть внутренний тип $$typeof — Symbol, который нельзя случайно перезаписать из пользовательского кода. Symbol — это гарантированно уникальный ключ.

Какую проблему решает

Когда нужно добавить свойство к чужому объекту (или к объекту из внешней библиотеки), строковые ключи могут конфликтовать. Symbol гарантирует уникальность: два Symbol с одинаковым описанием всё равно разные.

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

  • Объекты — ключи объектов
  • Типы данных — примитивные типы
  • Итерируемые — Symbol.iterator
  • Создание Symbol

    const id = Symbol('id')   // 'id' — описание только для отладки
    const id2 = Symbol('id')
    
    console.log(id === id2)          // false — всегда уникальны!
    console.log(typeof id)           // 'symbol'
    console.log(id.toString())       // 'Symbol(id)'
    console.log(id.description)      // 'id'

    Symbol как ключ объекта

    Символьные ключи не перечисляются в for...in, не возвращаются Object.keys(), JSON.stringify(), Object.entries():

    const TOKEN = Symbol('token')
    
    const user = {
      name: 'Иван',
      [TOKEN]: 'eyJhbGciOiJIUzI1NiJ9...'  // вычисляемый ключ с Symbol
    }
    
    console.log(user.name)     // 'Иван'
    console.log(user[TOKEN])   // 'eyJ...' — доступен если знаешь Symbol
    
    for (const key in user) {
      console.log(key)  // только 'name' — TOKEN не виден!
    }
    
    Object.keys(user)     // ['name']
    JSON.stringify(user)  // '{"name":"Иван"}' — Symbol исчез
    
    // Но получить можно явно:
    Object.getOwnPropertySymbols(user)  // [Symbol(token)]

    Symbol.for() — глобальный реестр

    Symbol.for(key) создаёт Symbol в глобальном реестре. Повторный вызов с тем же ключом возвращает тот же Symbol:

    const s1 = Symbol.for('app:theme')
    const s2 = Symbol.for('app:theme')
    console.log(s1 === s2)   // true — один и тот же Symbol!
    
    Symbol.keyFor(s1)        // 'app:theme'

    Используй Symbol.for когда один Symbol нужен в нескольких модулях.

    Well-known Symbol: Symbol.iterator

    Встроенные Symbol настраивают поведение объектов:

    // Symbol.iterator — делает объект итерируемым
    const range = {
      from: 1, to: 5,
      [Symbol.iterator]() {
        let current = this.from
        return {
          next: () => current <= this.to
            ? { value: current++, done: false }
            : { done: true, value: undefined }
        }
      }
    }
    
    console.log([...range])           // [1, 2, 3, 4, 5]
    for (const n of range) console.log(n)  // 1 2 3 4 5

    Well-known Symbol: Symbol.toPrimitive

    const price = {
      amount: 1500,
      currency: 'RUB',
      [Symbol.toPrimitive](hint) {
        if (hint === 'number') return this.amount
        if (hint === 'string') return this.amount + ' ' + this.currency
        return this.amount
      }
    }
    
    console.log(`Цена: ${price}`)  // 'Цена: 1500 RUB'
    console.log(+price)              // 1500
    console.log(price + 500)         // 2000

    Применение Symbol

  • Уникальные константы: гарантированно без конфликтов имён
  • Расширение чужих объектов: без риска сломать существующий код
  • «Приватные» метаданные: до появления private class fields (#)
  • Well-known symbols: кастомизация Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance
  • Типичные ошибки

    Ошибка 1: Symbol теряется — нет ссылки

    const obj = {}
    obj[Symbol('key')] = 'value'  // Symbol создан inline — ссылки нет!
    
    // Как получить это значение?
    const syms = Object.getOwnPropertySymbols(obj)  // [Symbol(key)]
    obj[syms[0]]  // 'value' — только так, через перебор
    
    // Правильно — сохраняй Symbol в переменную
    const KEY = Symbol('key')
    obj[KEY] = 'value'
    obj[KEY]  // 'value' — всегда доступно

    Ошибка 2: Symbol не конвертируется в строку автоматически

    const sym = Symbol('test')
    'Привет ' + sym   // TypeError: Cannot convert a Symbol value to a string
    
    // Правильно
    'Привет ' + sym.toString()   // 'Привет Symbol(test)'

    Ошибка 3: Symbol.for vs Symbol — путаница

    Symbol('id') === Symbol('id')        // false — всегда разные
    Symbol.for('id') === Symbol.for('id') // true — один и тот же из реестра

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

  • React: $$typeof = Symbol.for('react.element') защита от XSS
  • Node.js: встроенные библиотеки используют Symbol для служебных свойств
  • Протоколы: Symbol.iterator, Symbol.asyncIterator, Symbol.toPrimitive
  • Плагины: библиотеки добавляют к объектам скрытые метаданные через Symbol
  • Примеры

    Symbol для уникальных скрытых ключей и Symbol.iterator на пользовательском объекте

    // 1. Symbol как уникальный скрытый ключ — метаданные объекта
    const _id        = Symbol('id')
    const _createdAt = Symbol('createdAt')
    
    let counter = 0
    function createEntity(data) {
      return {
        ...data,
        [_id]: 'entity-' + (++counter),
        [_createdAt]: new Date().toISOString(),
      }
    }
    
    const user  = createEntity({ name: 'Алиса', email: 'alice@mail.ru' })
    const order = createEntity({ product: 'Ноутбук', price: 50000 })
    
    console.log(user.name)          // 'Алиса'
    console.log(user[_id])          // 'entity-1' — только если знаешь Symbol
    console.log(order[_id])         // 'entity-2'
    console.log(Object.keys(user))  // ['name', 'email'] — ID скрыты!
    
    // Проверяем что Symbol не попадает в JSON
    console.log(JSON.stringify(user))  // '{"name":"Алиса","email":"alice@mail.ru"}'
    
    // 2. Symbol.iterator — кастомная пагинация
    const pagedData = {
      pages: [
        ['Страница 1, элемент 1', 'Страница 1, элемент 2'],
        ['Страница 2, элемент 1'],
        ['Страница 3, элемент 1', 'Страница 3, элемент 2', 'Страница 3, элемент 3'],
      ],
      [Symbol.iterator]() {
        let pageIdx = 0
        let itemIdx = 0
        const pages = this.pages
        return {
          next() {
            if (pageIdx >= pages.length) return { done: true, value: undefined }
            const item = pages[pageIdx][itemIdx]
            itemIdx++
            if (itemIdx >= pages[pageIdx].length) { pageIdx++; itemIdx = 0 }
            return { value: item, done: false }
          }
        }
      }
    }
    
    const allItems = [...pagedData]
    console.log(allItems.length)   // 6 — все элементы со всех страниц
    console.log(allItems[0])       // 'Страница 1, элемент 1'

    Symbol

    В Node.js библиотека http добавляет на объект запроса служебные свойства — но не хочет конфликтовать с полями которые создаёт разработчик. В React у каждого JSX-элемента есть внутренний тип $$typeof — Symbol, который нельзя случайно перезаписать из пользовательского кода. Symbol — это гарантированно уникальный ключ.

    Какую проблему решает

    Когда нужно добавить свойство к чужому объекту (или к объекту из внешней библиотеки), строковые ключи могут конфликтовать. Symbol гарантирует уникальность: два Symbol с одинаковым описанием всё равно разные.

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

  • Объекты — ключи объектов
  • Типы данных — примитивные типы
  • Итерируемые — Symbol.iterator
  • Создание Symbol

    const id = Symbol('id')   // 'id' — описание только для отладки
    const id2 = Symbol('id')
    
    console.log(id === id2)          // false — всегда уникальны!
    console.log(typeof id)           // 'symbol'
    console.log(id.toString())       // 'Symbol(id)'
    console.log(id.description)      // 'id'

    Symbol как ключ объекта

    Символьные ключи не перечисляются в for...in, не возвращаются Object.keys(), JSON.stringify(), Object.entries():

    const TOKEN = Symbol('token')
    
    const user = {
      name: 'Иван',
      [TOKEN]: 'eyJhbGciOiJIUzI1NiJ9...'  // вычисляемый ключ с Symbol
    }
    
    console.log(user.name)     // 'Иван'
    console.log(user[TOKEN])   // 'eyJ...' — доступен если знаешь Symbol
    
    for (const key in user) {
      console.log(key)  // только 'name' — TOKEN не виден!
    }
    
    Object.keys(user)     // ['name']
    JSON.stringify(user)  // '{"name":"Иван"}' — Symbol исчез
    
    // Но получить можно явно:
    Object.getOwnPropertySymbols(user)  // [Symbol(token)]

    Symbol.for() — глобальный реестр

    Symbol.for(key) создаёт Symbol в глобальном реестре. Повторный вызов с тем же ключом возвращает тот же Symbol:

    const s1 = Symbol.for('app:theme')
    const s2 = Symbol.for('app:theme')
    console.log(s1 === s2)   // true — один и тот же Symbol!
    
    Symbol.keyFor(s1)        // 'app:theme'

    Используй Symbol.for когда один Symbol нужен в нескольких модулях.

    Well-known Symbol: Symbol.iterator

    Встроенные Symbol настраивают поведение объектов:

    // Symbol.iterator — делает объект итерируемым
    const range = {
      from: 1, to: 5,
      [Symbol.iterator]() {
        let current = this.from
        return {
          next: () => current <= this.to
            ? { value: current++, done: false }
            : { done: true, value: undefined }
        }
      }
    }
    
    console.log([...range])           // [1, 2, 3, 4, 5]
    for (const n of range) console.log(n)  // 1 2 3 4 5

    Well-known Symbol: Symbol.toPrimitive

    const price = {
      amount: 1500,
      currency: 'RUB',
      [Symbol.toPrimitive](hint) {
        if (hint === 'number') return this.amount
        if (hint === 'string') return this.amount + ' ' + this.currency
        return this.amount
      }
    }
    
    console.log(`Цена: ${price}`)  // 'Цена: 1500 RUB'
    console.log(+price)              // 1500
    console.log(price + 500)         // 2000

    Применение Symbol

  • Уникальные константы: гарантированно без конфликтов имён
  • Расширение чужих объектов: без риска сломать существующий код
  • «Приватные» метаданные: до появления private class fields (#)
  • Well-known symbols: кастомизация Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance
  • Типичные ошибки

    Ошибка 1: Symbol теряется — нет ссылки

    const obj = {}
    obj[Symbol('key')] = 'value'  // Symbol создан inline — ссылки нет!
    
    // Как получить это значение?
    const syms = Object.getOwnPropertySymbols(obj)  // [Symbol(key)]
    obj[syms[0]]  // 'value' — только так, через перебор
    
    // Правильно — сохраняй Symbol в переменную
    const KEY = Symbol('key')
    obj[KEY] = 'value'
    obj[KEY]  // 'value' — всегда доступно

    Ошибка 2: Symbol не конвертируется в строку автоматически

    const sym = Symbol('test')
    'Привет ' + sym   // TypeError: Cannot convert a Symbol value to a string
    
    // Правильно
    'Привет ' + sym.toString()   // 'Привет Symbol(test)'

    Ошибка 3: Symbol.for vs Symbol — путаница

    Symbol('id') === Symbol('id')        // false — всегда разные
    Symbol.for('id') === Symbol.for('id') // true — один и тот же из реестра

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

  • React: $$typeof = Symbol.for('react.element') защита от XSS
  • Node.js: встроенные библиотеки используют Symbol для служебных свойств
  • Протоколы: Symbol.iterator, Symbol.asyncIterator, Symbol.toPrimitive
  • Плагины: библиотеки добавляют к объектам скрытые метаданные через Symbol
  • Примеры

    Symbol для уникальных скрытых ключей и Symbol.iterator на пользовательском объекте

    // 1. Symbol как уникальный скрытый ключ — метаданные объекта
    const _id        = Symbol('id')
    const _createdAt = Symbol('createdAt')
    
    let counter = 0
    function createEntity(data) {
      return {
        ...data,
        [_id]: 'entity-' + (++counter),
        [_createdAt]: new Date().toISOString(),
      }
    }
    
    const user  = createEntity({ name: 'Алиса', email: 'alice@mail.ru' })
    const order = createEntity({ product: 'Ноутбук', price: 50000 })
    
    console.log(user.name)          // 'Алиса'
    console.log(user[_id])          // 'entity-1' — только если знаешь Symbol
    console.log(order[_id])         // 'entity-2'
    console.log(Object.keys(user))  // ['name', 'email'] — ID скрыты!
    
    // Проверяем что Symbol не попадает в JSON
    console.log(JSON.stringify(user))  // '{"name":"Алиса","email":"alice@mail.ru"}'
    
    // 2. Symbol.iterator — кастомная пагинация
    const pagedData = {
      pages: [
        ['Страница 1, элемент 1', 'Страница 1, элемент 2'],
        ['Страница 2, элемент 1'],
        ['Страница 3, элемент 1', 'Страница 3, элемент 2', 'Страница 3, элемент 3'],
      ],
      [Symbol.iterator]() {
        let pageIdx = 0
        let itemIdx = 0
        const pages = this.pages
        return {
          next() {
            if (pageIdx >= pages.length) return { done: true, value: undefined }
            const item = pages[pageIdx][itemIdx]
            itemIdx++
            if (itemIdx >= pages[pageIdx].length) { pageIdx++; itemIdx = 0 }
            return { value: item, done: false }
          }
        }
      }
    }
    
    const allItems = [...pagedData]
    console.log(allItems.length)   // 6 — все элементы со всех страниц
    console.log(allItems[0])       // 'Страница 1, элемент 1'

    Задание

    Создай модуль для работы с сущностями с символьными метаданными. Реализуй: функцию createWithId(obj) — добавляет скрытый Symbol-ключ с уникальным ID; функцию getId(obj) — возвращает ID; функцию hasId(obj) — проверяет наличие ID. Symbol должен быть создан один раз и недоступен снаружи (замыкание).

    Подсказка

    createWithId: { ...obj, [idKey]: ++_counter }. getId: return obj[idKey]. hasId: return idKey in obj. Symbol создан вне функций — он общий для всех трёх функций, но снаружи модуля его не видно.

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