Series: JavaScript Basics Lesson 37

Symbol

Symbol nima, nega yaratilgan, V8 uni xotirada qanday saqlaydi, Hidden Class bilan bog'liqligi, well-known Symbol'lar (Symbol.iterator, Symbol.toPrimitive) va Symbol.for() global registry.

37-dars

Symbol

1. Symbol nima va nega kerak?

ES6 gacha object property key faqat ikki xil bo'lishi mumkin edi — string yoki (avtomatik string ga aylantiriladigan) hech nima. Bu bir muammo tug'dirardi: agar siz kutubxona yozayotgan bo'lsangiz va object ga "yashirin" metadata qo'ymoqchi bo'lsangiz, qanday key ishlatasiz? Qanday nom tanlasangiz ham, foydalanuvchi undan bexabar xuddi shu nomdagi property yozib qo'yishi mumkin — name collision.

// Kutubxona yozuvchisi:
obj.__id__ = 'secret-123';

// Foydalanuvchi bilmagan holda:
obj.__id__ = 'boshqa narsa'; // konflikt!

Symbol shu muammoni hal qilish uchun yaratildi: har doim noyob (unique) qiymat yaratadigan yangi primitive tur.

const s1 = Symbol('id');
const s2 = Symbol('id');

s1 === s2; // false — description bir xil bo'lsa ham!

'id' — bu faqat description (debugging uchun), qiymatga ta'sir qilmaydi. Har Symbol() chaqirilganda butunlay yangi, boshqa hech narsaga teng bo'lmagan qiymat yaratiladi.

Symbol — bu primitive type (undefined, null, boolean, number, string, bigint, symbol). Lekin boshqa primitivlardan farqi shunda — u qiymat orqali emas, identity (o'ziga xoslik) orqali taqqoslanadi.


2. V8 xotirada Symbol'ni qanday ifodalaydi

V8 xotirada Symbol'ni quyidagicha ifodalaydi: har bir Symbol — bu heap dagi noyob obyekt-manzilga ega bo'lgan yorliq. Symbol('id') chaqirilganda V8:

  1. Yangi ichki Symbol obyekt yaratadi (unique identity bilan)
  2. description maydonini 'id' qiladi (faqat metama'lumot)
  3. Bu obyektga pointer qaytaradi
Symbol('id')  →  heap: [Symbol#0x1A2B { description: 'id' }]
Symbol('id')  →  heap: [Symbol#0x3F9C { description: 'id' }]

Ikkalasi ham description: 'id', lekin xotiradagi manzillari boshqa — shuning uchun === false qaytaradi.


3. Symbol property qanday saqlanadi (Hidden Class bilan bog'liqlik)

Oldin ko'rgan Hidden Class mexanizmini eslang — object property lar offset orqali saqlanadi. Symbol key lar ham xuddi shunday property sifatida saqlanadi, lekin V8 ularni alohida internal tableda tutadi — chunki Symbol key lar for...in, Object.keys(), JSON.stringify() da ko'rinmasligi kerak.

const idSymbol = Symbol('id');
const user = {
  name: 'Ali',
  [idSymbol]: 12345
};

Object.keys(user);        // ['name'] — symbol yo'q
JSON.stringify(user);     // '{"name":"Ali"}' — symbol yo'q
for (let k in user) { }   // faqat 'name' — symbol yo'q

Object.getOwnPropertySymbols(user); // [Symbol(id)] — faqat shu orqali ko'rinadi
Reflect.ownKeys(user);              // ['name', Symbol(id)] — hammasi

V8 ichida object ikkita alohida property storage ga ega bo'ladi: string-keyed properties (Hidden Class transition chain orqali) va symbol-keyed properties (alohida yashiringan qism). Shuning uchun oddiy iteratsiya (for...in, Object.keys) ularni ko'rmaydi — ular enumerable emas dizayn bo'yicha, lekin private ham emas (getOwnPropertySymbols orqali baribir olsa bo'ladi).

Muhim: Symbol — bu haqiqiy private emas. #privateField (class ichida) dan farqli, Symbol'ga refleksiya orqali yetib bo'ladi. Symbol maqsadi — collision oldini olish, security emas.


4. Well-known Symbols — JS engine "hook" lari

Bu qism eng muhim qism: Symbol faqat sizning kodingiz uchun emas, JS engine'ning o'zi foydalanadigan maxsus Symbol'lar bor. Ular Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance va hokazo.

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  // Bu — engine "for...of qachon ishlatilsa shu funksiyani chaqir" degan hook
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
}

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

for...of ichida V8 quyidagini qiladi: object'da Symbol.iterator bormi tekshiradi → bo'lsa, chaqiradi → qaytgan obyektning .next() metodini chaqiraverib, done: true bo'lguncha davom etadi.

Boshqa muhim well-known symbol'lar:

Symbol Vazifasi
Symbol.iterator for...of, spread (...) qanday iteratsiya qilishni belgilaydi
Symbol.toPrimitive obj + 1, `${obj}` kabi holatlarda obyekt qanday primitive'ga aylanishini belgilaydi
Symbol.toStringTag Object.prototype.toString.call(obj) natijasini o'zgartiradi
Symbol.hasInstance instanceof operatorining xatti-harakatini custom qiladi
Symbol.asyncIterator for await...of uchun
class Money {
  constructor(amount) { this.amount = amount; }

  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.amount;
    if (hint === 'string') return `$${this.amount}`;
    return `Money(${this.amount})`;
  }
}

const m = new Money(100);
console.log(+m);        // 100          (hint: 'number')
console.log(`${m}`);    // '$100'       (hint: 'string')
console.log(m + '');    // 'Money(100)' (hint: 'default')

Bu — nega ba'zi built-in obyektlar (Date, masalan) + yoki template string ichida "aqlli" ishlaydi, degan savolga javob. Ular ichida aynan shu Symbol.toPrimitive (yoki eski valueOf/toString) implementatsiya qilingan.


5. Symbol.for() — global registry

Oddiy Symbol() har doim yangi, noyob qiymat yaratadi. Lekin ba'zan sizga turli fayllar/modullar orasida bir xil Symbolni bo'lishish kerak bo'ladi. Shu uchun Symbol.for() bor:

const s1 = Symbol.for('app.id');
const s2 = Symbol.for('app.id');

s1 === s2; // true!

V8 buni global Symbol registry (engine darajasidagi bitta global Map<string, Symbol>) orqali amalga oshiradi:

Global Symbol Registry:
  'app.id' → Symbol#0x7C1D

Symbol.for('app.id') chaqirilganda: registry'da 'app.id' bormi tekshiriladi → bo'lsa, o'sha mavjud Symbol qaytariladi → bo'lmasa, yangi yaratiladi va registry'ga yoziladi. Bu registry butun JS runtime (hatto turli iframe/realm lar) bo'ylab umumiy, chunki u V8 process darajasida saqlanadi, oddiy module scope emas.

Symbol.keyFor(s1) — teskarisini qiladi, Symbol'dan registry key'ni topadi.


6. Symbol vs boshqa yechimlar — nega ayni shu kerak?

Yechim Muammosi
'_privateName' string key Collision xavfi, hech qanday himoya yo'q
WeakMap orqali private data Ishlaydi, lekin og'ir syntax, ko'p object uchun noqulay
#privateField (ES2022) Haqiqiy private, lekin faqat class ichida ishlaydi, dinamik key bo'la olmaydi
Symbol Collision'siz key, engine hook'lari uchun standart, lekin private emas

Xulosa

Symbol'ning asosiy o'rni — "metaprogramming key": object'ning "oddiy data"si bilan "maxsus xulq-atvor"ini key darajasida ajratish. Har bir Symbol() chaqiruvi heap'da noyob identity yaratadi, shuning uchun bir xil description'ga qaramay === doim false bo'ladi; V8 bu key'larni alohida internal tableda tutadi, shuning uchun ular for...in, Object.keys() va JSON.stringify() da ko'rinmaydi, lekin Object.getOwnPropertySymbols() orqali baribir olsa bo'ladi — ya'ni collision'dan himoyalaydi, ammo private emas. Well-known Symbol'lar (Symbol.iterator, Symbol.toPrimitive va h.k.) esa engine'ning o'zi tan oladigan "hook" lar bo'lib, for...of yoki + kabi operatorlarning custom obyektlar bilan qanday ishlashini belgilaydi, Symbol.for() esa global registry orqali modullar o'rtasida bitta Symbolni bo'lishish imkonini beradi.