IlmHamroh
JavaScript Full-stack/10-qism. JavaScript chuqur25/65-dars22 daqiqa
Mundarija (30)

JavaScript closure tuzoqlari: var li sikl, eskirgan closure va xotira

Qisqacha: Closure'ning uchta klassik tuzog'i bor. var li sikl — barcha closure'lar bitta o'zgaruvchini bo'lishadi va sikl oxiridagi qiymatni ko'radi; yechim — let (har aylanishda yangi o'zgaruvchi). Eskirgan closure — funksiya ma'lumotning eski nusxasini eslab qolgan, haqiqiy ma'lumot esa boshqa joyda o'zgargan; yechim — qiymatni chaqiruv paytida o'qish. Xotira — closure eslagan narsa tozalanmaydi; yechim — faqat keraklisini eslash va keraksiz tinglovchilarni olib tashlash.

Bu darsda

  • Nega var li siklda barcha funksiyalar bir xil qiymatni ko'rishini va let buni qanday tuzatishini tushuntira olasiz.
  • Eskirgan (stale) closure ni tanib, uni tuzata olasiz — React'dagi eng mashhur xatolardan birining ildizini bilasiz.
  • Closure katta ma'lumotni xotirada qanday ushlab qolishini o'lchab ko'rasiz va oldini olasiz.
  • vazifalar dagi closure kodini uch tuzoq bo'yicha tekshirib, refactor/closure branch'ini yakunlaysiz.

Oldin bilishingiz kerak: Closure: funksiya o'z muhitini eslab qoladi, Closure amaliyotda, var va nega undan voz kechildi, for sikli, Ichma-ich ma'lumotni immutable yangilash, Listener opsiyalari.

1. Nega bu kerak?

O'tgan ikki darsda closure'ning kuchini ko'rdik: hisoblagich, seyf, fabrika, once, kesh, debounce. Bu kuchning teskari tomoni ham bor. Closure "ko'rinmas" ishlaydi: kodda u haqida hech narsa yozilmaydi, u funksiya yaratilgan joyda o'zi paydo bo'ladi. Shuning uchun uning xatolari ham ko'rinmas — xato xabari chiqmaydi, faqat natija noto'g'ri bo'ladi.

Uchta tuzoq bor va ularning har biri real loyihalarda yillar davomida minglab dasturchini qiynagan:

  1. var li sikl — "uchta tugma, uchalasi ham '4' deydi". 2015-yilgacha eng mashhur JavaScript savoli.
  2. Eskirgan closure — "tugma eski ma'lumotni ko'rsatyapti". Bugun React dasturchilarining eng ko'p uchraydigan xatosi.
  3. Xotira — "sahifa bir soat ochiq tursa, sekinlashib qoladi".

for sikli darsida bir va'da bergan edik: "JavaScript har aylanish uchun i ning yangi nusxasini yaratadi. Keyinroq, kechiktirib bajariladigan kodda bu juda muhim bo'ladi." Bugun o'sha "keyinroq".

Hayotdan o'xshatish: uy kaliti. Kalit — foydali narsa. Lekin bir kalitni uch kishiga bersangiz, uchalasi bitta uyga kiradi (birinchi tuzoq). Eski uyning kalitini yangi uyga ko'chganingizdan keyin ham olib yursangiz, eski uyga borasiz (ikkinchi tuzoq). Kalitini hech kim qaytarmagan uyni esa sotib ham, buzib ham bo'lmaydi — u bo'sh turib joy egallaydi (uchinchi tuzoq).

2. Birinchi tuzoq: var li sikl

2.1 Uchta eslatma, bitta javob

Ofitsiant Otabek uchta stolga choy olib borishni eslatadigan funksiyalarni sikl bilan yasadi:

js
const eslatmalar = [];

for (var stol = 1; stol <= 3; stol++) {
  eslatmalar.push(() => console.log(`${stol}-stolga choy`));
}

eslatmalar.forEach((eslat) => eslat());

Konsolda:

text
4-stolga choy
4-stolga choy
4-stolga choy

Uchala eslatma "4-stol" dedi. 4-stol esa umuman yo'q! Kutilgani — 1, 2, 3.

2.2 Nega shunday?

Closure darsidagi ikki qoidani eslang:

  1. Closure qiymatni emas, o'zgaruvchining o'zini eslaydi.
  2. Bitta muhitda tug'ilgan funksiyalar shu muhitni bo'lishadi.

var blokni tanimaydi: for ichida e'lon qilinsa ham, u butun funksiya (yoki fayl) uchun bitta. Demak siklda bitta stol o'zgaruvchisi bor. Uchala arrow funksiya o'sha bitta stol ni esladi. Sikl tugaganda stol — 4 (4 <= 3 noto'g'ri bo'lgani uchun sikl to'xtadi). Eslatmalar keyinroq chaqirilganda uchalasi o'sha bitta o'zgaruvchiga qaradi — unda 4.

Keyingi ni bosing va stol faqat bitta ekaniga e'tibor bering:

Bu xato faqat funksiya keyinroq chaqirilganda ko'rinadi. Arrow sikl ichida darhol chaqirilganda (eslatmalar.push(...) o'rniga console.log(...)) har safar to'g'ri son chiqardi — chunki o'sha lahzada stol haqiqatan 1, 2, 3 edi. Kechiktirilgan chaqiruv — massivda saqlangan funksiya, taymer, tugma bosilishi — tuzoqni ochadi.

2.3 let — har aylanishga yangi o'zgaruvchi

Bitta so'zni almashtiramiz:

js
const eslatmalar = [];

for (let stol = 1; stol <= 3; stol++) {
  eslatmalar.push(() => console.log(`${stol}-stolga choy`));
}

eslatmalar.forEach((eslat) => eslat());

Konsolda:

text
1-stolga choy
2-stolga choy
3-stolga choy

let — blok o'zgaruvchisi. for sikli uchun JavaScript yana bir ish qiladi: har aylanish boshida yangi stol o'zgaruvchisini yaratadi va unga oldingi aylanishdagi qiymatni ko'chiradi. stol++ esa yangisida bajariladi. Natijada uchta aylanish — uchta alohida muhit — uchta alohida stol. Har arrow o'z aylanishining stol ini esladi.

Bu — for sikli darsidagi va'daning javobi: "har aylanish uchun yangi nusxa" aynan closure uchun kerak.

Diagrammada ikki holat bor. «var» qutisida uchta funksiya bitta o'zgaruvchiga qaraydi, «let» qutisida — har biri o'z nusxasiga:

flowchart LR
  subgraph V["var: bitta o'zgaruvchi"]
    f1["1-eslatma"] --> S["stol: 4"]
    f2["2-eslatma"] --> S
    f3["3-eslatma"] --> S
  end
  subgraph L["let: har aylanishga yangisi"]
    g1["1-eslatma"] --> L1["stol: 1"]
    g2["2-eslatma"] --> L2["stol: 2"]
    g3["3-eslatma"] --> L3["stol: 3"]
  end

2.4 Brauzerda: tugmalar

Real hayotda bu tuzoq ko'pincha tugmalarda chiqadi. Pastdagi oynada ikki qator tugma bor: yuqorisi var bilan, pastkisi let bilan yasalgan. Har birini bosib, konsolga qarang:

html
<p>var bilan:</p>
<div id="var-tugmalar"></div>
<p>let bilan:</p>
<div id="let-tugmalar"></div>
<script>
  const varQuti = document.getElementById("var-tugmalar");
  const letQuti = document.getElementById("let-tugmalar");

  for (var i = 1; i <= 3; i++) {
    const tugma = document.createElement("button");
    tugma.textContent = `${i}-stol`;
    tugma.addEventListener("click", () => {
      console.log(`var: ${i}-stolga choy`);
    });
    varQuti.append(tugma);
  }

  for (let j = 1; j <= 3; j++) {
    const tugma = document.createElement("button");
    tugma.textContent = `${j}-stol`;
    tugma.addEventListener("click", () => {
      console.log(`let: ${j}-stolga choy`);
    });
    letQuti.append(tugma);
  }
</script>

Yuqoridagi "1-stol" ni bossangiz — var: 4-stolga choy. Pastdagi "1-stol" — let: 1-stolga choy. Tugma yozuvi ikkalasida to'g'ri, chunki textContent sikl ichida, darhol o'rnatildi. Tinglovchi esa keyinroq, bosilganda ishlaydi — va var li variantda bitta i ga qaraydi.

09-qismda addEventListener darsida tugmalar to'g'ri ishlagani shu sababli edi: biz for...of va const ishlatgan edik. for...of ham har aylanishda yangi const yaratadi.

2.5 Eski kodda uchraydigan yechimlar

let 2015-yilda qo'shilgan. Undan oldin bu tuzoq boshqacha yechilgan — eski kodda va eski darsliklarda ko'rasiz:

js
const eslatmalar = [];

for (var stol = 1; stol <= 3; stol++) {
  ((raqam) => {
    eslatmalar.push(() => console.log(`${raqam}-stolga choy`));
  })(stol);
}

eslatmalar.forEach((eslat) => eslat());

Konsolda:

text
1-stolga choy
2-stolga choy
3-stolga choy

Har aylanishda IIFE chaqirildi — yangi chaqiruv, yangi muhit. raqam parametri o'sha lahzadagi stol qiymatini oldi va arrow uni esladi. Ishlaydi, lekin o'qish qiyin. Yana bir eski usul — forEach: uning callback'i har element uchun alohida chaqiriladi, demak har biri o'z muhitiga ega.

Bugungi qoida oddiy: sikl o'zgaruvchisi — doim let (yoki for...of da const). var ni ko'rsangiz — almashtiring (var darsi).

Tekshirib ko'ring: let stol; sikldan oldin e'lon qilinib, sikl for (stol = 1; stol <= 3; stol++) deb yozilsa, eslatmalar nima deydi?

Javob

Uchalasi "4-stol" — xuddi var dagidek. Har aylanishda yangi o'zgaruvchi faqat let sikl sarlavhasida e'lon qilinganda yaratiladi. Tashqarida e'lon qilingan let stol — bitta o'zgaruvchi, sikl uni faqat o'zgartiradi. Demak qoida "let yozish" emas, "let ni sikl ichida e'lon qilish".

3. Ikkinchi tuzoq: eskirgan closure

3.1 Closure "jonli" edi-ku?

Closure darsida closure jonli ekanini ko'rdik: o'zgaruvchi o'zgarsa, closure yangi qiymatni ko'radi. Unda qanday qilib u "eskirishi" mumkin?

Javob: closure eslagan o'zgaruvchi endi o'zgarmay qolgan bo'lsa. Haqiqiy ma'lumot esa boshqa joyda yangilanib turgan bo'lsa. Mana oddiy holat — buyurtma holatini 100 ms dan keyin xabar qiladigan eslatma:

js
function eslatmaRejala(buyurtma) {
  const holati = buyurtma.holati;
  setTimeout(() => {
    console.log(`Buyurtma #${buyurtma.id}: ${holati}`);
  }, 100);
}

const buyurtma = { id: 7, holati: "tayyorlanmoqda" };
eslatmaRejala(buyurtma);
buyurtma.holati = "yetkazildi";

Konsolda:

text
Buyurtma #7: tayyorlanmoqda

Buyurtma allaqachon yetkazilgan, eslatma esa "tayyorlanmoqda" deydi. Closure jonli: u holati o'zgaruvchisini eslaydi. Lekin holati — const, u eslatma rejalangan lahzadagi satrni oldi va boshqa hech qachon o'zgarmaydi. O'zgargan narsa — buyurtma.holati — closure qaramaydigan joyda.

Eskirgan closure (stale closure) — funksiya ma'lumotning eski nusxasini saqlaydigan o'zgaruvchini eslagan, haqiqiy ma'lumot esa yangilangan holat. Inglizcha "stale" — "qotib qolgan, eskirgan" (eskirgan non haqida ham shunday deyiladi).

Tuzatish: qiymatni nusxalamang, chaqiruv paytida o'qing — ${buyurtma.holati}. Endi closure buyurtma obyektini eslaydi va 100 ms dan keyin uning hozirgi holatiga qaraydi: "yetkazildi".

3.2 Immutable yangilashda

Ikkinchi shakl ko'proq uchraydi va ko'proq aldaydi. Immutable yangilash darsida holatni o'zgartirish o'rniga yangi obyekt yasashni o'rgandik. Endi shu usulni closure bilan birga ko'ramiz:

js
let holat = { soni: 0 };

function savatniChiz() {
  const joriy = holat;
  return () => console.log(`Savatda: ${joriy.soni} ta`);
}

const tugmaBosildi = savatniChiz();
holat = { ...holat, soni: 3 };
tugmaBosildi(); // Savatda: 0 ta

Bu safar joriy — obyekt, havola. Lekin holat = { ...holat, soni: 3 } eski obyektni o'zgartirmadi — yangi obyekt yasab, holat ni unga qaratdi. joriy esa hamon eski obyektga qarab turibdi, unda soni: 0.

flowchart LR
  T["tugmaBosildi<br/>(closure)"] --> J["joriy"]
  J --> E["eski obyekt<br/>soni: 0"]
  H["holat"] --> Y["yangi obyekt<br/>soni: 3"]

Closure eski obyektni ushlab qoldi, holat esa yangisiga ko'chdi. Ikkalasi o'rtasida endi hech qanday bog'lanish yo'q.

3.3 React'ga ko'prik

Nega bu tuzoq bugun shunchalik mashhur? Chunki React aynan shu tarzda ishlaydi. React'da komponent — funksiya, u har chizishda qaytadan chaqiriladi va o'sha chizishdagi holat bilan yangi tinglovchilar yasaydi. Holat esa immutable yangilanadi. Har chizish — o'z "suratini" (snapshot) olgan closure'lar to'plami.

Agar eski chizishdagi funksiya — masalan, taymer — hali ishlab tursa, u eski suratdagi holatni ko'radi. Hisoblagich 1 da qotib qoladi, chat eski xabarlarni ko'rsatadi. React'da buning maxsus yechimlari bor (yangilovchi funksiya, bog'liqliklar ro'yxati), ularni 18-qismda o'rganamiz — hozir bilish shart emas. Asosiysi, ildizi bugungi savatniChiz misolining o'zi: closure eski obyektni eslab qolgan.

3.4 Uch yechim

  1. Chaqiruv paytida o'qing. Nusxa olmang, ma'lumot turgan "konteyner"ni eslang: () => console.log(holat.soni) — holat o'zgaruvchisining o'zi, nusxasi emas.
  2. Yangi qiymatni argument bilan bering. Funksiya ma'lumotni eslamasin, chaqirilganda olsin: korsat(holat.soni).
  3. Ma'lumot o'zgarganda closure'ni qayta yasang. Holat yangilandi — eski funksiyani tashlab, yangisini yasang. React shunday qiladi.

Teskari holat ham bor: ba'zan eski qiymat kerak. Masalan, "o'zgarishdan oldin nima edi?" — buni ataylab nusxalash to'g'ri. vazifalar da shunday joy bor, uni «Vazifalar qadami» da topasiz. Tuzoq — nusxa olishning o'zi emas, uni bilmasdan olish.

Tekshirib ko'ring: savatniChiz ni return () => console.log(Savatda: ${holat.soni} ta); deb o'zgartirsak, tugmaBosildi() nima chiqaradi?

Javob

Savatda: 3 ta. Endi closure holat o'zgaruvchisini eslaydi. Chaqiruv paytida holat yangi obyektga qaragan — soni: 3. Bu — birinchi yechim: ma'lumotni chaqiruv paytida o'qish.

4. Uchinchi tuzoq: xotira

4.1 Closure eslagan narsa tozalanmaydi

JavaScript xotirani o'zi tozalaydi: hech kim ishlatmayotgan qiymat avtomatik o'chiriladi. Bu jarayon axlat yig'ish (garbage collection) deyiladi. Lekin closure eslagan narsa "ishlatilmoqda" deb hisoblanadi — funksiya yashar ekan, u ham yashaydi. Ko'pincha bu aynan kerakli narsa. Ba'zan esa closure keraksiz katta ma'lumotni ushlab qoladi.

Hisobot funksiyasini ko'raylik. U million qatorli jadvalni oladi, lekin keyin faqat qatorlar sonini ko'rsatadi. Ikki variantni o'lchaymiz: o'nta shunday hisobot yasab, xotira qancha o'sishiga qaraymiz. Birinchi to'rt qator va xotiraMB funksiyasi — o'lchov asboblari. Ular axlat yig'ishni majburan ishga tushiradi va Node'dan band xotira hajmini so'raydi (process — Node'ning o'z obyekti). import qatorlarini Nega modullar darsida o'rganamiz — hozir bu qismni tushunish shart emas, faqat oxirgi +N MB natijasiga qarang.

js
import v8 from "node:v8";
import vm from "node:vm";
v8.setFlagsFromString("--expose-gc");
const tozala = vm.runInNewContext("gc");

function xotiraMB() {
  tozala();
  return Math.round(process.memoryUsage().heapUsed / 1e6);
}

function hisobotYarat(qatorlar) {
  return () => `Hisobotda ${qatorlar.length} ta qator`;
}

function jadvalOch() {
  const qatorlar = new Array(1_000_000).fill(0);
  return hisobotYarat(qatorlar);
}

const boshi = xotiraMB();
const hisobotlar = [];
for (let i = 0; i < 10; i++) {
  hisobotlar.push(jadvalOch());
}
console.log(hisobotlar[0]());
console.log(`+${xotiraMB() - boshi} MB`);

Konsolda:

text
Hisobotda 1000000 ta qator
+80 MB

Har hisobot o'z jadvalini — million elementli massivni — eslab qoldi. O'nta hisobot — 80 MB. Bizga esa har biridan faqat bitta son kerak edi!

Tuzatish — closure'ga faqat keraklisini bering. Sonni tashqarida hisoblab, closure'da faqat uni qoldiramiz:

js
import v8 from "node:v8";
import vm from "node:vm";
v8.setFlagsFromString("--expose-gc");
const tozala = vm.runInNewContext("gc");

function xotiraMB() {
  tozala();
  return Math.round(process.memoryUsage().heapUsed / 1e6);
}

function hisobotYarat(qatorlar) {
  const soni = qatorlar.length;
  return () => `Hisobotda ${soni} ta qator`;
}

function jadvalOch() {
  const qatorlar = new Array(1_000_000).fill(0);
  return hisobotYarat(qatorlar);
}

const boshi = xotiraMB();
const hisobotlar = [];
for (let i = 0; i < 10; i++) {
  hisobotlar.push(jadvalOch());
}
console.log(hisobotlar[0]());
console.log(`+${xotiraMB() - boshi} MB`);

Konsolda:

text
Hisobotda 1000000 ta qator
+0 MB

Natija bir xil, xotira — deyarli nol. Arrow endi faqat soni ni ishlatadi. Chrome va Node'ning JavaScript dvigateli (V8) ishlatilmaydigan qatorlar ni orqa cho'ntakka solmaydi — Closure darsida DevTools'da ko'rgan optimizatsiya. Massivlar tozalandi.

10 ta hisobot closure'i qancha xotira ushlab turadi
  • Butun jadvalni eslaydi10 × 1 000 000 element80 MB
  • Faqat sonini eslaydimassivlar tozalandi0 MB

Manba: O'lchandi: Node.js 24.21, heapUsed axlat yig'ishdan keyin, 2026-10-04

4.2 Bitta muhit — umumiy xotira

V8'ning optimizatsiyasida bitta muhim nozik joy bor. Bitta funksiya ichida bir nechta closure tug'ilsa, ular bitta umumiy "orqa cho'ntak"ni bo'lishadi. Bittasi katta ma'lumotni ishlatsa, cho'ntakka u tushadi — va qolganlari ham uni ushlab turadi:

js
function sahifaniSozla() {
  const katta = new Array(1_000_000).fill(0);
  const ism = "Bahor";

  function hisobla() {
    return katta.length;
  }
  console.log(hisobla());

  return () => ism;
}

const nomniOl = sahifaniSozla();
console.dir(nomniOl);

Chrome Console'da nomniOl ning [[Scopes]] ini oching:

text
[[Scopes]]: Scopes[3]
  0: Closure (sahifaniSozla) {katta: Array(1000000), ism: 'Bahor'}
  1: Script {nomniOl: ƒ}
  2: Global {window: Window, …}

nomniOl faqat ism ni ishlatadi, lekin uning cho'ntagida katta ham bor. Sababi — hisobla funksiyasi: u katta ni ishlatdi, ikkala funksiya bitta muhitda tug'ilgan, cho'ntak bitta. hisobla allaqachon ishlab bo'lgan va unga hech kim havola qilmaydi — baribir nomniOl yashar ekan, million elementli massiv ham yashaydi.

Bu — til qoidasi emas, dvigatelning xulqi. Lekin u real loyihalarda bir necha mashhur xotira sizib chiqishiga (memory leak) sabab bo'lgan. Himoya: katta ma'lumot bilan ishlaydigan kodni alohida funksiyaga chiqaring yoki ishlatib bo'lgach o'zgaruvchini tozalang (katta = null, let bilan e'lon qilingan bo'lsa).

4.3 Hech qachon olib tashlanmaydigan tinglovchi

Xotira tuzog'ining eng ko'p uchraydigan shakli — hodisa tinglovchisi. Tinglovchi — closure. U window yoki document ga qo'shilsa, sahifa yopilguncha yashaydi va eslagan hamma narsani ushlab turadi:

js
function oynaniOch(malumot) {
  window.addEventListener("resize", () => {
    console.log(`${malumot.length} ta qator qayta chizildi`);
  });
}

Har oynaniOch chaqiruvi window ga yangi tinglovchi qo'shadi. Oyna yopilsa ham, tinglovchi window da qoladi — malumot bilan birga. Mehmon oynani o'n marta ochib-yopsa — o'nta tinglovchi va o'nta jadval xotirada, har resize da esa o'nta qayta chizish.

Listener opsiyalari darsidagi yechim: signal. Oyna yopilganda boshqaruv.abort() chaqiriladi va tinglovchi olib tashlanadi — u bilan birga closure va malumot ham tozalanadi:

js
function oynaniOch(malumot) {
  const boshqaruv = new AbortController();
  window.addEventListener(
    "resize",
    () => console.log(`${malumot.length} ta qator qayta chizildi`),
    { signal: boshqaruv.signal },
  );
  return () => boshqaruv.abort();
}

// jadval — oynada ko'rsatiladigan katta massiv
const oynaniYop = oynaniOch(jadval);
// ... mehmon oynani yopdi:
oynaniYop();

E'tibor bering: oynaniOch qaytargan "yopuvchi" funksiya ham closure — u boshqaruv ni eslaydi. Closure muammoni yaratdi, closure uni yechdi ham.

Shu ro'yxatga Closure amaliyotda darsidagi kesh ham kiradi: u har yangi argumentni saqlaydi va hech narsani o'chirmaydi. Uzoq ishlaydigan dasturda keshga chegara qo'yiladi ("oxirgi 100 ta") yoki maxsus "kuchsiz" to'plamlar ishlatiladi — ularni WeakMap va WeakRef darsida ko'ramiz.

Tekshirib ko'ring: function sanoqchiYarat(buyurtmalar) { return () => buyurtmalar.length; } — closure butun massivni ushlab turadimi? Qanday tuzatasiz?

Javob

Ha — arrow buyurtmalar ni ishlatadi, demak butun massiv cho'ntakda. Agar bizga faqat o'sha paytdagi son kerak bo'lsa: const soni = buyurtmalar.length; return () => soni;. Lekin diqqat — bu endi "surat": massiv keyin o'ssa, closure eski sonni ko'rsatadi (ikkinchi tuzoq!). Har doim jonli son kerak bo'lsa, massivni eslash to'g'ri. Qaysi biri kerakligini vazifaning o'zi hal qiladi — tuzoq ikkalasining farqini bilmaslikda.

5. Ko'p uchraydigan xatolar

5.1 setTimeout va var

js
for (var urinish = 1; urinish <= 3; urinish++) {
  setTimeout(() => console.log(`${urinish}-urinish`), 0);
}

Konsolda:

text
4-urinish
4-urinish
4-urinish

Intervyuning klassik savoli. setTimeout callback'i sikl tugagandan keyin ishlaydi — 0 ms bo'lsa ham. Hozircha shuni eslab qoling: taymer callback'i joriy kod to'liq tugamaguncha navbatda kutadi. Nega aynan shunday — Event loop darsida ko'ramiz. Sikl tugaganda urinish — 4, uchala callback o'sha bitta o'zgaruvchiga qaraydi. Tuzatish: let urinish.

5.2 let sikldan tashqarida

«let — har aylanishga yangi o'zgaruvchi» bo'limidagi savol: let i; oldin e'lon qilinib, sikl uni faqat o'zgartirsa — bitta o'zgaruvchi, var bilan bir xil natija. Tuzatish: for (let i = 1; ...) — e'lon sikl sarlavhasida bo'lsin.

5.3 Destructuring bilan "surat" olish

js
let holat = { filtr: "hammasi" };

const { filtr } = holat;
const filtrniAyt = () => console.log(filtr);

holat = { ...holat, filtr: "faol" };
filtrniAyt(); // hammasi

Destructuring — qisqa va chiroyli, lekin u qiymatni nusxalaydi. filtr — mustaqil o'zgaruvchi, holat yangilansa ham u o'zgarmaydi. Bu — eskirgan closure'ning ko'zga eng kam tashlanadigan shakli. Tuzatish: o'zgaradigan holatni chaqiruv paytida o'qing — () => console.log(holat.filtr).

5.4 Har ochilishda yangi tinglovchi

«Hech qachon olib tashlanmaydigan tinglovchi» bo'limidagi holat. Belgisi: biror amal har takrorlanganda konsolga chiqadigan qatorlar soni ko'payib boradi (1, 2, 3…). Bu — tinglovchi har safar qo'shilyapti, lekin olib tashlanmayapti. Tuzatish: { signal } yoki removeEventListener bilan tozalang, yoki tinglovchini bir marta, funksiyadan tashqarida qo'shing.

6. Mashqlar

1-mashq (oson): Navbat tablosi

Kod har stol uchun "Stol N tayyor" deydigan funksiyalarni yasashi kerak edi, lekin hammasi "Stol 6 tayyor" deyapti. Toping va bitta so'z bilan tuzating.

js
const tablolar = [];
for (var n = 1; n <= 5; n++) {
  tablolar.push(() => `Stol ${n} tayyor`);
}
console.log(tablolar[0]());
Yechim
js
const tablolar = [];
for (let n = 1; n <= 5; n++) {
  tablolar.push(() => `Stol ${n} tayyor`);
}
console.log(tablolar[0]()); // Stol 1 tayyor
console.log(tablolar[4]()); // Stol 5 tayyor

var → let. Endi har aylanish o'z n iga ega. var bilan esa bitta n bor edi va sikl oxirida u 6 bo'lgan (6 <= 5 noto'g'ri — to'xtash).

2-mashq (o'rta): Narx o'zgardi, chek — yo'q

Kassir chekni 50 ms dan keyin chiqaradi. Shu orada oshpaz narxni yangiladi. Chek eski narxni ko'rsatyapti. Chek chiqarilgan paytdagi narxni ko'rsatsin.

js
const menyu = { osh: 35000 };

function chekniRejala(taom) {
  const narx = menyu[taom];
  setTimeout(() => console.log(`${taom}: ${narx} so'm`), 50);
}

chekniRejala("osh");
menyu.osh = 38000;
Yechim
js
const menyu = { osh: 35000 };

function chekniRejala(taom) {
  setTimeout(() => console.log(`${taom}: ${menyu[taom]} so'm`), 50);
}

chekniRejala("osh");
menyu.osh = 38000;

Konsolda:

text
osh: 38000 so'm

const narx — rejalashtirilgan paytdagi surat edi. Endi closure menyu va taom ni eslaydi va narxni chek chiqarilayotganda o'qiydi. Qaysi biri to'g'ri — biznes qoidasi hal qiladi: ba'zi tizimlarda chek buyurtma paytidagi narxni ko'rsatishi shart. Unda surat — to'g'ri tanlov, faqat buni ongli qiling.

3-mashq (qiyin): Uch tuzoqni toping

Quyidagi kodda uchala tuzoq bor. Har birini toping, nima noto'g'ri chiqishini ayting va tuzating.

js
let holat = { mehmonlar: ["Ali", "Malika", "Bobur"] };
const salomlar = [];

function salomlarniYasa(arxiv) {
  const { mehmonlar } = holat;
  for (var i = 0; i < mehmonlar.length; i++) {
    salomlar.push(
      () => `Salom, ${mehmonlar[i]}! Arxiv: ${arxiv.length}`,
    );
  }
}

salomlarniYasa(new Array(1_000_000).fill("eski buyurtma"));
holat = { mehmonlar: [...holat.mehmonlar, "Jasur"] };
console.log(salomlar[0]());
Yechim
  1. var i — uchala salom bitta i ni ko'radi, sikl oxirida i = 3. mehmonlar[3] — undefined: "Salom, undefined!". Tuzatish — let i.
  2. const { mehmonlar } = holat — surat. "Jasur" qo'shilgan yangi obyekt salomlar ga ko'rinmaydi. Bu yerda bu to'g'ri bo'lishi ham mumkin (salomlar yasalgan paytdagi mehmonlar uchun), lekin buni ongli tanlang.
  3. arxiv — million elementli massiv. Har salom faqat uning uzunligini ishlatadi, lekin butun massivni ushlab turadi. Tuzatish — uzunlikni oldindan oling.
js
let holat = { mehmonlar: ["Ali", "Malika", "Bobur"] };
const salomlar = [];

function salomlarniYasa(arxiv) {
  const { mehmonlar } = holat;
  const arxivSoni = arxiv.length;
  for (let i = 0; i < mehmonlar.length; i++) {
    salomlar.push(
      () => `Salom, ${mehmonlar[i]}! Arxiv: ${arxivSoni}`,
    );
  }
}

salomlarniYasa(new Array(1_000_000).fill("eski buyurtma"));
holat = { mehmonlar: [...holat.mehmonlar, "Jasur"] };
console.log(salomlar[0]()); // Salom, Ali! Arxiv: 1000000

Birinchi va uchinchi tuzoq — aniq xato edi. Ikkinchisi — qaror: biz suratni qoldirdik, chunki salomlar yasalgan paytdagi mehmonlarga tegishli.

4-mashq: Vazifalar qadami — tuzoqlarni tekshiring va branch'ni yakunlang

refactor/closure branch'ida id generatori ulangan (Closure amaliyotda). Bugungi uch tuzoq bo'yicha tekshiramiz va branch'ni main ga qo'shamiz.

1. Noto'g'ri joydagi generator. Jamoadoshingiz shunday variant taklif qildi — "generator faqat vazifaQosh ga kerak, o'sha yerda yasaylik":

js
function vazifaQosh(matn) {
  const yangiId = idYasovchiYarat(4);
  holat.vazifalar = [
    ...holat.vazifalar,
    { id: yangiId(), matn, bajarildi: false },
  ];
  // ...
}

Uch marta vazifa qo'shilsa, qanday id'lar chiqadi? Node'da tekshiring.

2. Audit. vazifalar.js ni uchta savol bilan ko'rib chiqing: var bormi? Sikl ichida tinglovchi qo'shiladimi? Qayerda qiymat "surat" qilib olinadi va bu ataylabmi?

Yechim

1.

js
function idYasovchiYarat(boshlangich) {
  let keyingi = boshlangich;
  return () => keyingi++;
}

const vazifalar = [];
function vazifaQosh(matn) {
  const yangiId = idYasovchiYarat(4);
  vazifalar.push({ id: yangiId(), matn });
}

vazifaQosh("Non olish");
vazifaQosh("Choy damlash");
vazifaQosh("Kitob o'qish");
console.log(vazifalar.map((v) => v.id)); // [ 4, 4, 4 ]

Uchalasi 4! Har vazifaQosh chaqiruvi idYasovchiYarat(4) ni qayta chaqirdi — yangi muhit, yangi keyingi = 4. Bu — Closure darsidagi "yasovchini har safar qayta chaqirish" xatosi, faqat oqibati og'irroq: takroriy id'lar. O'tgan darsda holat.keyingiId = 1 bilan ko'rgan "boshqa vazifa belgilanadi" xatosi qaytadi. Generator ilova uchun bitta va faylning yuqori darajasida yasaladi — hozirgi kodingizdagidek.

2. Audit natijasi:

Savol vazifalar.js da Xulosa
var bormi? Yo'q — const va let
Sikl ichida tinglovchi? Yo'q — delegation, har joyda bitta tinglovchi (Event delegation) birinchi va uchinchi tuzoq yo'q
Surat qayerda? royxat tinglovchisida const zaxira = qoshniId(id) ataylab

Uchinchi qatorga e'tibor bering. "O'chirish" bosilganda kod avval qo'shni vazifaning id sini oladi, keyin vazifani o'chiradi va fokusni o'sha qo'shniga qo'yadi. O'chirishdan keyin qo'shnini qidirsak, qator allaqachon yo'q bo'lardi. Bu — ongli surat: bizga aynan o'zgarishdan oldingi holat kerak.

Boshqa closure'lar — tinglovchilar, fokusla ichidagi top, yangiId — holat, royxat kabi o'zgaruvchilarni chaqiruv paytida o'qiydi. Eskirish xavfi yo'q.

3. Yakunlash. Brauzerda oxirgi marta to'liq sinang (qo'shish, o'chirib yana qo'shish — id takrorlanmaydi, belgilash, tahrirlash, filtrlar, fokus), keyin:

bash
git switch main
git merge refactor/closure
git branch -d refactor/closure
git push

Pages bir-ikki daqiqada yangilanadi. Foydalanuvchi hech qanday farq ko'rmaydi — ilova ichida esa endi holat da keyingiId yo'q, id'larni hech kim tasodifan buzolmaydi.

7. Real ishda

  • React. "Stale closure" — React savollar saytlarida eng ko'p so'raladigan mavzulardan biri. useEffect ichidagi taymer eski holatni ko'radi, bog'liqliklar ro'yxati unutilgan — ildizi bugungi savatniChiz. React'ni 17–18-qismlarda o'rganganingizda bu dars sizga katta ustunlik beradi.
  • Xotira sizib chiqishi. Uzoq ochiq turadigan ilovalarda (Gmail, Telegram Web, admin panellar) tozalanmagan tinglovchi va katta closure'lar soatlab to'planib, sahifani sekinlashtiradi. Chrome DevTools'ning Memory paneli shuni qidirish uchun — uni Xotira sizishlari va DevTools: Performance, Memory va Network darslarida ko'ramiz.
  • Kod ko'rib chiqish. Tajribali dasturchi siklda var ni, tinglovchida removeEventListener siz addEventListener ni va katta obyektni eslab qolgan callback'ni darhol belgilaydi.
  • Intervyu. for (var i...) setTimeout(...) — eng klassik JavaScript savollaridan. Endi javobni ham, sababini ham (bitta o'zgaruvchi), ikki yechimini ham (let va IIFE) bilasiz. Javob sikl boshlanishiga bog'liq: i = 0; i < 3 bo'lsa — 3 3 3, bizdagidek 1 dan <= 3 gacha bo'lsa — 4 4 4.

Xulosa

  • var li siklda barcha closure'lar bitta o'zgaruvchini eslaydi va sikl oxiridagi qiymatni ko'radi.
  • for (let ...) har aylanishda yangi o'zgaruvchi yaratadi — har closure o'zinikini eslaydi. let sikl sarlavhasida e'lon qilinsin.
  • Eskirgan closure — funksiya ma'lumotning eski nusxasini (surat) eslagan. Yechim: chaqiruv paytida o'qish, argument bilan berish yoki closure'ni qayta yasash.
  • Ataylab olingan surat — xato emas; tuzoq — uni bilmasdan olish.
  • Closure eslagan narsa xotiradan tozalanmaydi. Faqat keraklisini eslang; bitta muhitdagi closure'lar cho'ntakni bo'lishishini unutmang.
  • Tinglovchi ham closure: keraksiz bo'lganda { signal } yoki removeEventListener bilan olib tashlang.

Keyingi dars: Global obyekt va globalThis — scope zanjirining eng tashqi qavati: global obyekt nima, brauzerda va Node'da u qanday ataladi va unga yozish nega xavfli.

Manbalar

  • MDN: "Closures" — "Creating closures in loops: A common mistake", "Performance considerations" — developer.mozilla.org
  • MDN: "Memory management" — developer.mozilla.org
  • ECMAScript 2025 Language Specification: "CreatePerIterationEnvironment" — tc39.es/ecma262
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript closure tuzoqlari: var li sikl, eskirgan closure va xotira — IlmHamroh