Symbol

Symbol — примитивный тип данных, значения которого гарантированно уникальны. Используется как идентификатор свойств объекта.

Зачем нужно

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

Где используется

  • Уникальные ключи свойств объектов
  • Метапрограммирование (well-known symbols)
  • Скрытые свойства (не видны в for...in)
  • Реализация итераторов (Symbol.iterator)

Предпосылки

Типы данных, Объекты

Создание Symbol

// Каждый Symbol уникален
const sym1 = Symbol;
const sym2 = Symbol;
console.log(sym1 === sym2); // false

// Описание (для отладки)
const id = Symbol('id');
const name = Symbol('id'); // Описание одинаковое...
console.log(id === name);  // false — но символы разные!
console.log(id.toString()); // "Symbol(id)"
console.log(id.description); // "id"

// Symbol — НЕ конструктор
// const sym = new Symbol(); // TypeError!

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

const id = Symbol('id');

const user = {
  name: 'Алиса',
  [id]: 42 // символьное свойство
};

console.log(user[id]); // 42
console.log(user.id);  // undefined — это другое свойство!

// Символьные свойства скрыты от обычного перебора
console.log(Object.keys(user));        // ['name']
console.log(JSON.stringify(user));     // '{"name":"Алиса"}'
for (const key in user) {
  console.log(key); // только 'name'
}

// Но их можно получить явно
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
console.log(Reflect.ownKeys(user)); // ['name', Symbol(id)]

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

// Symbol.for ищет или создаёт символ в глобальном реестре
const s1 = Symbol.for('app.id');
const s2 = Symbol.for('app.id');
console.log(s1 === s2); // true — один и тот же символ!

// Symbol.keyFor — получить ключ из реестра
console.log(Symbol.keyFor(s1)); // "app.id"

// Обычные символы не в реестре
const local = Symbol('local');
console.log(Symbol.keyFor(local)); // undefined

Well-known Symbols (Встроенные символы)

// Symbol.iterator — определяет итерацию
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator] {
    let current = this.from;
    const last = this.to;
    return {
      next {
        return current <= last
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
};

for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// Symbol.toPrimitive — кастомное преобразование типов
const money = {
  amount: 100,
  currency: 'RUB',
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.amount;
    if (hint === 'string') return `${this.amount} ${this.currency}`;
    return this.amount; // default
  }
};

console.log(+money);        // 100
console.log(`${money}`);    // "100 RUB"
console.log(money + 50);    // 150

// Symbol.hasInstance — кастомный instanceof
class Even {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0;
  }
}
console.log(4 instanceof Even); // true
console.log(3 instanceof Even); // false

// Symbol.toStringTag — кастомный Object.prototype.toString()
class MyClass {
  get [Symbol.toStringTag] {
    return 'MyClass';
  }
}
console.log(Object.prototype.toString().call(new MyClass));
// "[object MyClass]"

Практическое применение

Приватные свойства (до появления #private)

const _balance = Symbol('balance');

class BankAccount {
  constructor(initial) {
    this[_balance] = initial;
  }

  deposit(amount) {
    this[_balance] += amount;
  }

  get balance {
    return this[_balance];
  }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.balance); // 1500
console.log(account._balance); // undefined — не доступно по строке

Избежание коллизий

// Библиотека A добавляет свойство
const libA_id = Symbol('id');
function enrichByLibA(obj) {
  obj[libA_id] = 'data from A';
}

// Библиотека B тоже добавляет 'id'
const libB_id = Symbol('id');
function enrichByLibB(obj) {
  obj[libB_id] = 'data from B';
}

const obj = {};
enrichByLibA(obj);
enrichByLibB(obj);
// Обе библиотеки работают без конфликтов
console.log(obj[libA_id]); // 'data from A'
console.log(obj[libB_id]); // 'data from B'

Частые ошибки

1. Symbol не конвертируется в строку неявно

const sym = Symbol('test');
// console.log('Symbol: ' + sym); // TypeError!
console.log('Symbol: ' + sym.toString()); // OK
console.log(`Symbol: ${sym.description}`); // OK

2. Путаница Symbol и Symbol.for

Symbol('id') === Symbol('id');       // false — разные
Symbol.for('id') === Symbol.for('id'); // true — из реестра

Практика

  1. Создай объект с символьным свойством и покажи, что оно скрыто от Object.keys()
  2. Реализуй Symbol.iterator для объекта, который перебирает чётные числа от 0 до N
  3. Используй Symbol.toPrimitive для объекта Temperature с автоматическим преобразованием

Связанные темы

Ресурсы


🎓 Источник: Proxy и Symbol в JavaScript

  • 📅 2018-11-26 · YouTube
  • Тезисы: Symbol — уникальный идентификатор. Well-known symbols (Symbol.iterator, Symbol.toPrimitive) — это протоколы языка для меточек интеграции с runtime. Symbol.for — глобальный shared registry

🎓 Источник: Архив 2018 - Часть 5 EventEmitter, Symbol, Proxy

  • 📅 2020-01-05 · YouTube
  • Тезисы: Symbol — это "имя без коллизий". В EventEmitter можно использовать Symbol как тип события — гарантия отсутствия конфликта с другими подписчиками

🎓 Источник: Explicit resource management with Using and Symbol.dispose

  • 📅 2025-05-22 · YouTube
  • Тезисы: новый proposal — using statement + Symbol.dispose / Symbol.asyncDispose. Аналог IDisposable из C#, RAII из C++. Автоматически вызывает dispose при выходе из блока