Seriya: JavaScript Basics Dars 35

WeakMap

Map'ning kuchli referensi memory leak'ga qanday olib keladi, WeakMap qanday yechadi, V8 GC Mark-and-Sweep bilan bog'liqligi va DOM/private-data amaliy holatlar.

35-dars

WeakMap

1. Muammoni ko'rish — nega oddiy Map yetarli emas

let obj = { data: 'katta ma\'lumot' };
const map = new Map();
map.set(obj, 'metama\'lumot');

obj = null; // asosiy referensni o'chirdik

console.log([...map.keys()][0]); // { data: 'katta ma\'lumot' } — HALI HAM XOTIRADA!

Nima bo'lyapti? obj = null — bu faqat obj degan o'zgaruvchining obyektga bo'lgan pointerini o'chiradi. Lekin map — hali ham o'sha obyektga ishora qilib turibdi (mapning ichki hash table'ida). Garbage Collector (GC) — bir obyektni faqat unga hech kim ishora qilmay qolganda yig'ib oladi. map ishora qilib turgani uchun, obyekt hech qachon yig'ilmaydi — garchi dasturchi uni "kerak emas" deb hisoblasa ham.


2. WeakMap — yechim

let obj = { data: 'katta ma\'lumot' };
const weakMap = new WeakMap();
weakMap.set(obj, 'metama\'lumot');

obj = null; // asosiy referensni o'chirdik

// ENDI — GC keyingi tsiklida obyektni YIG'IB OLADI,
// chunki WeakMap unga "KUCHSIZ (weak)" ishora qiladi

WeakMap — obyektga "kuchsiz" (weak) referens bilan ishora qiladi. Bu — GC uchun "men bu obyektga ishora qilaman, lekin bu meni obyektni tirik saqlashga majburlamaydi" degani. Agar boshqa hech kim (hech qanday "kuchli" referens) obyektga ishora qilmasa — GC uni erkin yig'ib olishi mumkin, WeakMap'dagi yozuv bilan birga.


3. V8 ichida — Garbage Collector qanday ishlaydi (zarur kontekst)

V8 — Mark-and-Sweep algoritmidan foydalanadi:

1. MARK bosqichi: "Root"lardan (global obyekt, hozirgi Call Stack'dagi
   o'zgaruvchilar) boshlab, BARCHA yetib boriladigan obyektlarni "tirik" deb belgilaydi.

2. SWEEP bosqichi: Belgilanmagan (yetib bo'lmaydigan) obyektlarni XOTIRADAN o'chiradi.
ROOT (global/stack)
   │
   ▼
 obj  ───────────────→  { data: 'katta' }  ← Bu yerga yetib borish mumkin, TIRIK
   │
   ▼
 map.set(obj, ...) — map ICHIDA ham shu obyektga pointer bor

Agar obj = null qilinsa, lekin map hali ham unga ishora qilsa:

ROOT (global/stack)
   │
   × (obj o'chirildi, endi Root'dan to'g'ridan-to'g'ri yo'l yo'q)

 map  ───────────────→  { data: 'katta' }  ← MAP ORQALI HALI HAM YETIB BO'LADI, TIRIK!

mapning o'zi — Root'dan (masalan global o'zgaruvchi sifatida) yetib boriladi, va map obyektga ishora qilib turgani uchun, obyekt hali ham "yetib boriladigan" (reachable) hisoblanadi — GC uni o'chira olmaydi.


4. WeakMap'da GC nima uchun farqli ishlaydi

WeakMap — bu oddiy JS obyekti emas, bu V8 engine ichida maxsus, alohida implementatsiya qilingan tuzilma. GC algoritmi WeakMap ichidagi pointerlarni "mark" bosqichida umuman hisobga olmaydi:

GC MARK bosqichida:
- Oddiy Map ichidagi barcha key/value'lar → "tirik" deb belgilanadi (chunki Map ORQALI
  yetib borish HISOBGA OLINADI)
- WeakMap ichidagi key'lar → "tirik" deb belgilashda HISOBGA OLINMAYDI
  (bu pointer "ko'rinmas" — GC uni chetlab o'tadi)

Shu sababli, agar objga faqat WeakMap orqaligina yetib borilsa (boshqa hech qanday "kuchli" yo'l bo'lmasa) — GC uni yig'ib oladi, va WeakMap'dagi mos yozuv avtomatik, o'zi yo'qoladi.

ROOT
  × (obj = null qilindi)

weakMap ──╌╌╌╌╌╌╌╌╌→ { data: 'katta' }  ← "KUCHSIZ" chiziq (weak reference),
                                            GC buni KO'RMAYDI/hisobga OLMAYDI

  → GC bu obyektni "yetib bo'lmaydigan" deb topadi → YIG'IB OLADI
  → weakMap ichidagi yozuv HAM avtomatik yo'qoladi

5. WeakMap — nega faqat obyekt key qabul qiladi (bu tasodifiy emas!)

const weakMap = new WeakMap();

weakMap.set({ id: 1 }, 'qiymat'); // ✅ ishlaydi — obyekt key
weakMap.set('string', 'qiymat');  // ❌ TypeError: Invalid value used as weak map key
weakMap.set(42, 'qiymat');        // ❌ TypeError

Nega faqat obyekt? Chunki "weak reference" tushunchasi — faqat heap'da yashaydigan, GC tomonidan boshqariladigan narsalar uchun ma'no anglatadi. Primitivlar (string, number) — qiymat orqali ishlaydi, ular GC tomonidan "yig'iladigan obyekt" sifatida track qilinmaydi (ular yoki stack'da, yoki string bo'lsa V8'ning ichki intern-pool'ida saqlanishi mumkin, lekin bu "reachability" grafida obyekt sifatida ishtirok etmaydi).

Obyekt → Heap'da, GC tomonidan track qilinadi → "weak reference" MANTIQIY

Primitiv (string, number) → qiymat sifatida ko'chiriladi,
                              GC "reachability" grafida alohida "node" emas
                              → "weak reference" TUSHUNCHASI MA'NOSIZ

6. WeakMap metodlari — nimalar YO'Q va nega

weakMap.set(key, 'qiymat');
weakMap.get(key);      // 'qiymat'
weakMap.has(key);      // true
weakMap.delete(key);   // true

// LEKIN — bular YO'Q:
weakMap.size;                // undefined! — size mavjud emas
weakMap.forEach(...);        // TypeError — forEach yo'q
for (const x of weakMap) {}  // TypeError — iterable EMAS
[...weakMap];                // TypeError

Nega bunday cheklovlar bor — bu ataylab qilingan dizayn qarori: Map'da size, iteratsiya (for...of) ishlaydi, chunki Map'ning barcha elementlari doim "kuchli", barqaror — ularni sanash yoki aylanib chiqish har doim bir xil natija beradi.

WeakMap'da esa — GC istalgan paytda, dasturchi bilmagan holda, biror key'ni "yig'ib olishi" mumkin (chunki weak reference). Agar weakMap.size mavjud bo'lganida edi, bu qiymat har lahzada, dastur nazoratisiz o'zgarib turardi (GC ishlagan sari kamayib turadi) — bu predictable emas, nedeterministik xatti-harakat bo'lardi, va dasturchilarni chalkashtirardi. Shu sababli spetsifikatsiya ataylab size, iteratsiya imkoniyatlarini olib tashlagan — bu xatolik emas, xavfsizlik uchun ataylab qilingan cheklov.


7. Amaliy holat — DOM elementga vaqtinchalik ma'lumot biriktirish

const elementData = new WeakMap();

function attachData(el, data) {
  elementData.set(el, data);
}

let button = document.querySelector('#myButton');
attachData(button, { clicks: 0, lastClicked: null });

// Foydalanuvchi sahifadan elementni olib tashlaydi:
button.remove();
button = null; // JS tomonidagi so'nggi referens ham o'chirildi

// ENDI — GC button elementini XOTIRADAN yig'ib oladi,
// va elementData ichidagi mos yozuv HAM AVTOMATIK yo'qoladi.
// Agar oddiy Map ishlatilganida, button HECH QACHON yig'ilmasdi,
// garchi u DOM'dan olib tashlangan bo'lsa ham — MEMORY LEAK!

Bu — WeakMap'ning eng klassik, real-world ishlatilish holati: DOM elementlarga (yoki boshqa "vaqtinchalik" obyektlarga) qo'shimcha ma'lumot biriktirish, lekin elementning o'zi olib tashlanganda, bog'langan ma'lumot ham avtomatik, memory leak'siz tozalanishi kerak bo'lganda.


8. WeakMap — private data saqlash uchun (Private Field'dan OLDIN ishlatilgan pattern)

# (private field) paydo bo'lishidan oldin (ES2022'dan avval), WeakMap xuddi shu maqsad uchun ishlatilardi:

const _balance = new WeakMap(); // TASHQI, modul darajasidagi WeakMap

class Account {
  constructor(balance) {
    _balance.set(this, balance); // 'this' — KEY sifatida!
  }

  getBalance() {
    return _balance.get(this);
  }
}

const acc = new Account(1000);
console.log(acc.getBalance()); // 1000
console.log(acc._balance);     // undefined — tashqaridan ko'rinmaydi!

Bu qanday ishlaydi: har bir Account instance (this) — _balance WeakMap'da key sifatida ishlatiladi, va uning shaxsiy balansi value sifatida saqlanadi. Instance yo'q qilinsa (acc = null) — GC uni yig'ib oladi, va _balance WeakMap'dagi mos yozuv ham avtomatik yo'qoladi — memory leak yo'q.

Nega # private field bundan afzalroq (zamonaviy kodda)? Chunki WeakMap pattern — tashqi, modul darajasidagi o'zgaruvchi talab qiladi (_balance), bu biroz noqulay va "boilerplate" hisoblanadi. #field — bu logikani class ichiga, toza sintaksis bilan joylashtiradi. Lekin ikkalasi ham — oxir-oqibat, xuddi shu maqsadga (haqiqiy maxfiylik) xizmat qiladi.


9. WeakMap vs Map — to'liq solishtirish jadvali

Map WeakMap
Key turi Istalgan (primitiv ham, obyekt ham) Faqat obyekt (yoki Symbol, ba'zi versiyalarda)
Referens turi Kuchli (strong) — GC'ni to'xtatadi Kuchsiz (weak) — GC'ga to'siq bo'lmaydi
size ✅ Bor ❌ Yo'q
Iteratsiya (for...of, forEach) ✅ Bor ❌ Yo'q
clear() ✅ Bor ❌ Yo'q
Memory leak xavfi Bor (agar delete unutilsa) Yo'q (avtomatik tozalanadi)
Ishlatilish holati Umumiy key-value store Obyektga bog'liq, vaqtinchalik metama'lumot

10. Nega iteratsiya/size yo'qligi — GC bilan chambarchas bog'liq (chuqur tushuntirish)

GC odatda darhol ishlamaydi — u davriy ravishda, V8 "qulay" deb topgan paytda ishga tushadi (masalan Call Stack bo'sh bo'lganda, yoki xotira bosimi oshganda). Bu shuni anglatadiki:

let obj = { data: 1 };
weakMap.set(obj, 'meta');
obj = null;

// BU YERDA — obyekt HALI YIG'ILMAGAN bo'lishi mumkin!
// GC hali ishlamagan, chunki u DARHOL emas, DAVRIY ishlaydi

Agar weakMap.size mavjud bo'lganida, uning qiymati GC qachon ishlashiga bog'liq bo'lib qolardi — bir vaqt 1, GC ishlagandan keyin 0, lekin aniq qachon o'zgarishini dasturchi bashorat qila olmaydi. Bu — nedeterministik API bo'lardi, va JS spetsifikatsiyasi har doim, barcha enginelarda bir xil xatti-harakat kafolatlashga intiladi — shuning uchun bunday "GC vaqtiga bog'liq" xususiyat ataylab, spetsifikatsiya darajasida taqiqlangan.


Xulosa

WeakMap — Mapning faqat obyekt key qabul qiladigan, GC-friendly varianti: u key'larga kuchsiz (weak) referens bilan ishora qiladi, ya'ni Garbage Collector'ning "mark" bosqichida bu pointer hisobga olinmaydi — agar obyektga boshqa hech qanday "kuchli" yo'l qolmasa, GC uni erkin yig'ib oladi, va WeakMap'dagi mos yozuv avtomatik, dasturchi aralashuvisiz yo'qoladi. Aynan shu nedeterministik tozalanish tufayli, size va iteratsiya imkoniyatlari ataylab olib tashlangan — chunki ular GC vaqtiga bog'liq, bashorat qilib bo'lmaydigan natija berardi.