IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati23/44-dars22 daqiqa
Mundarija (42)

JavaScript immutability chuqur: Object.freeze, structuredClone va structural sharing

Qisqacha: Immutability — ma'lumotni joyida o'zgartirmaslik: o'zgarish kerak bo'lsa, yangi nusxa yasaladi, eskisi tegilmay qoladi. Shunda "nima o'zgardi?" savoliga bitta === javob beradi, "bekor qilish" esa o'z-o'zidan ishlaydi. Bu qimmat emas: faqat o'zgargan qism yangidan yasaladi, qolgani eski va yangi holatda umumiy (structural sharing). Object.freeze va structuredClone ning chegaralari bor — darsda ularni aniq ko'ramiz. Immer kutubxonasi esa "joyida o'zgartirgandek" yozib, o'zgarmas natija olishga imkon beradi.

Bu darsda

  • Immutability'ning beshta amaliy foydasini va narxini aytib bera olasiz.
  • Obyekt, massiv, Map va Set ni o'zgartirmasdan yangilash naqshlarini qo'llaysiz.
  • Object.freeze va structuredClone qayerda yetmasligini aniq misollar bilan bilasiz.
  • Structural sharing nega arzonligini o'lchov bilan tushuntira olasiz.
  • Immer qanday ishlashini va uning ichida Proxy turganini tushunasiz.

Oldin bilishingiz kerak: Ichma-ich ma'lumotni immutable yangilash, Obyektni himoyalash: freeze, seal, preventExtensions, FP g'oyalari va deklarativ uslub, Proxy va Reflect.

1. Nega bu kerak?

Oldingi darsda FP g'oyalarini ko'rdik va uchinchisini — "ma'lumot o'zgartirilmaydi" — keyinga qoldirdik. Bugun shu g'oyani oxirigacha ochamiz.

Ikki so'zni eslab olamiz. Mutatsiya (mutation) — mavjud obyekt yoki massivning ichini joyida o'zgartirish: push, obj.x = 1, delete. Immutability (o'zgarmaslik) — buning teskarisi: ma'lumot yaratilgandan keyin o'zgartirilmaydi, o'zgarish kerak bo'lsa — yangi nusxa yasaladi.

Siz buni amalda bilasiz. Immutable yangilash darsida ichma-ich obyektni yo'l bo'ylab spread bilan yangiladingiz. freeze darsida obyektni muzlatdingiz. Lekin ikki savol ochiq qoldi: nega bunga shuncha kuch sarflash kerak? Va bu vositalar qayerda pand beradi?

«Bahor»dagi yangi voqea. Stajyor dasturchi Sardor kassa dasturiga "bekor qilish" tugmasini qo'shdi. U har o'zgarishdan oldin holatni tarixga saqladi:

js
const tarix = [];
const holat = { buyurtma: { taomlar: ["Osh"] } };

tarix.push(holat);
holat.buyurtma.taomlar.push("Manti");
tarix.push(holat);

console.log(tarix[0].buyurtma.taomlar); // [ 'Osh', 'Manti' ]
console.log(tarix[0] === tarix[1]); // true

Tarixda ikkita yozuv bor — lekin ikkalasi ham bitta obyekt. Sabab: tarix.push(holat) obyektning nusxasini emas, unga havolani saqladi (Havola semantikasi). Keyingi push("Manti") o'sha obyektni o'zgartirdi — demak, "tarixdagi" yozuv ham o'zgardi. "Bekor qilish" bosilsa, eski holat o'rniga yana o'sha yangi holat chiqadi. Tarix — ishlamaydigan bezak.

Hayotdan o'xshatish: bankdagi hisob ko'chirmasi (vypiska). Bank hech qachon kechagi qatorni o'chirib, ustiga yangi summani yozmaydi. Har amal — yangi qator. Shuning uchun istalgan kungi qoldiqni ko'rish mumkin va hech kim "kecha qancha edi?" deb bahslashmaydi. Immutable ma'lumot — xuddi shu ko'chirma: yozilgan narsa o'zgarmaydi, o'zgarish — yangi yozuv.

2. Nega immutability: beshta foyda

2.1 O'zgarishni bir zumda sezish

Ma'lumot joyida o'zgartirilmasa, "o'zgardimi?" degan savolga === javob beradi. Yangi obyekt — demak o'zgargan. O'sha obyekt — demak o'zgarmagan, ichiga qarash shart emas:

js
let chizishlarSoni = 0;
let oxirgiHolat = null;

function chiz(holat) {
  if (holat === oxirgiHolat) return;
  oxirgiHolat = holat;
  chizishlarSoni++;
}

const h1 = { filtr: "hammasi", vazifalar: [] };
chiz(h1);
chiz(h1);
const h2 = { ...h1, filtr: "faol" };
chiz(h2);

console.log(chizishlarSoni); // 2

Ikkinchi chiz(h1) hech narsa qilmadi — holat o'sha. h2 esa yangi obyekt, demak o'zgarish bor. Ichma-ich maydonlarni birma-bir solishtirish shart emas: bitta === yetarli.

Mutatsiya bo'lsa, bu tekshiruv aldanadi: h1.filtr = "faol"; chiz(h1) — obyekt o'sha, chiz hech narsa qilmaydi, ekran eskirib qoladi. React (interfeys qurish kutubxonasi, 17-qismda o'rganamiz) aynan shu sababdan holatni joyida o'zgartirishni "ko'rmaydi".

2.2 Tarix va "bekor qilish" bepul

Har o'zgarish yangi holat yasasa, eski holatlar o'z-o'zidan saqlanadi. Sardorning tarixini to'g'rilaymiz:

js
const tarix = [{ buyurtma: { taomlar: ["Osh"] } }];

const oxirgi = tarix.at(-1);
tarix.push({
  ...oxirgi,
  buyurtma: {
    ...oxirgi.buyurtma,
    taomlar: [...oxirgi.buyurtma.taomlar, "Manti"],
  },
});

console.log(tarix[0].buyurtma.taomlar); // [ 'Osh' ]
console.log(tarix[1].buyurtma.taomlar); // [ 'Osh', 'Manti' ]

Endi "bekor qilish" — shunchaki oldingi elementga qaytish. Mashhur vositalar ham shu g'oyada ishlaydi. Masalan, Redux (ilova holatini saqlash kutubxonasi) ning brauzer qo'shimchasida "vaqt bo'ylab sayohat" (time travel debugging) bor: har holat saqlangan, istalganiga qaytib, ilova o'sha paytda qanday ko'ringanini ko'rish mumkin. Redux'ni keyingi qismlarda o'rganamiz.

2.3 Kutilmagan o'zgarishdan himoya

Funksiyaga obyekt berdingiz — u uni o'zgartirib qo'ydimi? Mutatsiyali kodda buni bilish uchun funksiyaning ichini o'qish kerak. Immutable kelishuvda javob oldindan ma'lum: hech kim hech narsani joyida o'zgartirmaydi. Bu xatoni Havola semantikasi darsida ko'rgan edik: nusxa deb o'ylagan narsa aslida o'sha obyekt bo'lib chiqdi.

2.4 Asinxron koddagi xavfsizlik

Asinxron kodda await dan oldin va keyin holat boshqacha bo'lishi mumkin: kutish paytida boshqa kod uni o'zgartiradi. Immutable holatda esa siz ushlab turgan obyekt hech qachon o'zgarmaydi:

js
const holat = { vazifalar: ["Non olish"] };

async function saqla(nusxa) {
  await new Promise((tayyor) => setTimeout(tayyor, 100));
  console.log("Saqlandi:", nusxa.vazifalar);
}

const jarayon = saqla(holat);
const yangiHolat = {
  ...holat,
  vazifalar: [...holat.vazifalar, "Choy"],
};
await jarayon;
console.log("Hozir:", yangiHolat.vazifalar);

Konsolda:

text
Saqlandi: [ 'Non olish' ]
Hozir: [ 'Non olish', 'Choy' ]

saqla 100 ms kutdi, shu orada holat yangilandi. Lekin saqla qo'lidagi obyektni hech kim joyida o'zgartirmadi — yangilanish yangi obyektga yozildi. Shuning uchun u aynan chaqirilgan paytdagi holatni saqladi. Agar holat.vazifalar.push("Choy") yozilganda, saqlangan ro'yxatga kutilmaganda "Choy" ham tushib qolardi — race condition ning eng sodda ko'rinishi.

2.5 Xatoni topish oson

Holat faqat yangi nusxa orqali o'zgarsa, har o'zgarishning aniq joyi bor: yangi obyekt yasalgan qator. Mutatsiyali kodda esa xususiyatni kodning istalgan joyi o'zgartirgan bo'lishi mumkin.

2.6 Narxi

Halol bo'laylik, immutability tekin emas:

  • Ko'proq obyekt. Har o'zgarish — yangi obyektlar. Ular xotirani egallaydi, eskilarini axlat yig'uvchi tozalaydi (Xotira boshqaruvi).
  • Ko'proq yozuv. Uch qavatli spread — mutatsiyaning bitta qatoriga nisbatan uzun va xatoga moyil.
  • Intizom. Til buni majburlamaydi. Bitta push — va kelishuv buzildi.

Birinchi narxni structural sharing kamaytiradi, ikkinchisini — Immer. Uchinchisiga — freeze va testlar. Bularni birma-bir ko'ramiz.

Tekshirib ko'ring: «O'zgarishni bir zumda sezish» bo'limidagi chiz funksiyasi ichma-ich obyektni to'liq solishtirish o'rniga faqat === ishlatadi. Bu qachon xato natija beradi?

Javob

Kimdir holatni joyida o'zgartirsa: obyekt o'sha, === true, chiz "o'zgarish yo'q" deb hech narsa qilmaydi — ekran eskiradi. === tekshiruvi faqat hamma immutable kelishuvga amal qilganda ishonchli. Shuning uchun bu ikki narsa doim birga yuradi.

3. Naqshlar: har tur uchun yangilash

Immutable yangilash darsida obyekt va massiv naqshlarini ko'rgan edik. Bu yerda ularni Map, Set va xususiyatni o'chirish bilan to'ldiramiz:

Amal Mutatsiya () Immutable ()
Xususiyat qo'shish obj.x = 1 { ...obj, x: 1 }
Xususiyatni o'chirish delete obj.x const { x, ...qolgan } = obj
Massivga qo'shish arr.push(a) [...arr, a]
Massivda almashtirish arr[i] = a arr.with(i, a)
Massivdan olib tashlash arr.splice(i, 1) arr.toSpliced(i, 1)
Map ga yozish map.set(k, v) new Map(map).set(k, v)
Set ga qo'shish set.add(a) new Set(set).add(a)

Ikki yangi naqshni ishlatib ko'ramiz:

js
const mehmon = {
  ism: "Dilshod aka",
  tel: "+998 90 000 00 00",
  _ichki: 7,
};
const { _ichki, ...ochiqMalumot } = mehmon;

console.log(Object.keys(ochiqMalumot)); // [ 'ism', 'tel' ]
console.log("_ichki" in mehmon); // true

const narxlar = new Map([["osh", 35000], ["manti", 30000]]);
const yangiNarxlar = new Map(narxlar).set("osh", 38000);

const ikkalasi = [narxlar.get("osh"), yangiNarxlar.get("osh")];
console.log(ikkalasi); // [ 35000, 38000 ]

Birinchi naqsh — destructuring va rest: kerakmas kalit alohida o'zgaruvchiga "olinadi", qolgani yangi obyektga yig'iladi. Asl mehmon tegilmadi.

Ikkinchisi: new Map(narxlar) — sayoz nusxa, keyin set nusxaga yoziladi. set Map ning o'zini qaytaradi, shuning uchun bir qatorda yozdik. Set uchun ham xuddi shunday, yoki to'plam metodlari — union, difference — ular ham doim yangi Set qaytaradi.

Jadvaldan to'ldiring: massivdagi bitta elementni asl massivga tegmasdan almashtiradigan metod — .

4. Object.freeze chegaralari

freeze darsida uning asosiy chegarasini ko'rdik: u sayoz — ichma-ich obyektlarni muzlatmaydi. Bundan tashqari to'rt chegarasi bor. Ular kamroq ma'lum va shuning uchun xavfliroq.

4.1 Map, Set va Date muzlamaydi

js
const narxlar = Object.freeze(new Map([["osh", 35000]]));
narxlar.set("osh", 99000);

const ochilish = Object.freeze(new Date("2026-10-05T02:00:00Z"));
ochilish.setUTCHours(5);

console.log(narxlar.get("osh")); // 99000
console.log(ochilish.toISOString()); // 2026-10-05T05:00:00.000Z
console.log(Object.isFrozen(narxlar)); // true

isFrozen true deydi — lekin narx ham, vaqt ham o'zgardi, xato ham chiqmadi. Sabab Proxy darsidagi bilan bir xil: Map va Date ma'lumotni xususiyatlarda emas, ichki slotda saqlaydi. Ichki slot — obyekt ichidagi, koddan ko'rinmaydigan yashirin joy. freeze esa faqat ko'rinadigan xususiyatlarni qotiradi. set va setUTCHours ichki slotga yozadi — freeze uni ko'rmaydi.

Holatda Map yoki Date bo'lsa, ularni muzlatish befoyda. Ularni immutable kelishuv bilan ishlating (new Map(eski).set(...)) yoki holatda oddiy obyekt va ISO satr saqlang.

4.2 Private maydonlar ham

Klassning # maydonlari ham xususiyat emas:

js
class Hisoblagich {
  #soni = 0;
  oshir() {
    this.#soni++;
    return this.#soni;
  }
}

const muzlagan = Object.freeze(new Hisoblagich());

console.log(muzlagan.oshir()); // 1
console.log(muzlagan.oshir()); // 2

Obyekt "muzlagan", lekin ichki holati o'zgaryapti. Private maydonlar obyektga emas, klassning maxfiy jadvaliga tegishli — freeze u yerga yetmaydi.

4.3 Typed array'ni muzlatib bo'lmaydi

js
Object.freeze(new Uint8Array([1, 2, 3]));
text
TypeError: Cannot freeze array buffer views with elements

Tarjimasi: "Elementli array buffer ko'rinishlarini muzlatib bo'lmaydi." Typed array elementlari ham xususiyat emas, xotiradagi baytlar — ularni "faqat o'qiladigan" qilish mumkin emas. Bu holatda freeze hech bo'lmaganda ochiq xato beradi.

4.4 freeze — kafolat emas, signal

Xulosa: freeze oddiy obyekt va massiv uchun yaxshi himoya, lekin "bu ma'lumot o'zgarmas" degan to'liq kafolat emas. Shuning uchun amalda ikki yondashuv bor:

  • Ishlab chiqishda muzlatish. Kutubxonalar (masalan, Immer) holatni avtomatik muzlatadi. Kimdir tasodifan mutatsiya qilsa — qat'iy rejimda TypeError darhol chiqadi. Bu xatoni ertaroq topish vositasi.
  • Kompilyatsiya paytidagi tekshiruv. TypeScript'da readonly va as const — kod ishga tushishidan oldin "bu yerga yozib bo'lmaydi" deb ogohlantiradi. TypeScript'ni 15-qismda o'rganamiz, hozir bilish shart emas.

Tekshirib ko'ring: const s = Object.freeze(new Set(["osh"])); dan keyin s.add("manti") yozildi. Xato chiqadimi? s.size nechaga teng?

Javob

Xato chiqmaydi, s.size — 2. Set ham Map kabi elementlarini ichki slotda saqlaydi. freeze faqat xususiyatlarni qotiradi, add esa slotga yozadi. Shuning uchun Set ni muzlatish o'rniga new Set(s).add("manti") naqshini ishlating.

5. structuredClone chegaralari

Sayoz va chuqur nusxalash darsida structuredClone ni chuqur nusxaning eng yaxshi vositasi deb tanishgan edik. U haqiqatan ham kuchli: Date, Map, Set, RegExp, typed array'larni to'g'ri nusxalaydi, hatto aylanma havolalarni ham (JSON chuqur darsida JSON bu yerda yiqilgan edi). Lekin u nimalarni yo'qotishini bilish kerak.

5.1 Funksiyalar — xato

js
const taom = {
  nom: "Osh",
  narxiniHisobla() { return 35000; },
};

structuredClone(taom);
text
DataCloneError: narxiniHisobla() { return 35000; } could not be cloned.

Tarjimasi: "narxiniHisobla() {...} nusxalanmaydi." Xabarda funksiyaning butun matni ko'rsatiladi. Chrome'da xabar oldidan yana bir bo'lak bor: Failed to execute 'structuredClone' on 'Window':. Funksiya — kod, ma'lumot emas. structuredClone esa faqat ma'lumotni nusxalaydi.

5.2 Klass nusxasi oddiy obyektga aylanadi

js
class Taom {
  constructor(nom, narx) {
    this.nom = nom;
    this.narx = narx;
  }
  chegirma(foiz) {
    return this.narx * (1 - foiz / 100);
  }
}

const asl = new Taom("Osh", 35000);
const nusxa = structuredClone(asl);

console.log(nusxa); // { nom: 'Osh', narx: 35000 }
console.log(nusxa instanceof Taom); // false
console.log(typeof nusxa.chegirma); // undefined

Xato chiqmadi — bu yomonroq. Nusxa endi Taom emas: prototip yo'qoldi, u bilan birga metodlar ham. nusxa.chegirma(10) keyinroq, boshqa joyda TypeError beradi va sababini topish qiyin bo'ladi.

5.3 Yana nimalar yo'qoladi

Nima structuredClone dan keyin
Funksiya, metod DataCloneError
Klass prototipi yo'qoladi — oddiy obyekt
Getter oddiy qiymatga aylanadi (bir marta hisoblanadi)
Symbol kalitli xususiyat tushib qoladi
Sanalmaydigan xususiyat tushib qoladi
Object.freeze yo'qoladi — nusxa muzlamagan
DOM elementi DataCloneError

Oxirgi qatordan oldingisi muhim: muzlatilgan holatni structuredClone qilsangiz, nusxa muzlamagan bo'ladi. Muzlatish — obyektning xususiyati, ma'lumotning emas.

5.4 Qachon nima

Vaziyat Vosita
Ilova holatidagi bitta maydonni yangilash yo'l bo'ylab spread (structural sharing)
Butunlay mustaqil nusxa kerak (shablon, "nusxa olish" tugmasi) structuredClone
Ma'lumotni serverga yuborish yoki saqlash JSON.stringify
Metodli klass nusxasi klassning o'z nusxa() metodi yoki konstruktor

Tekshirib ko'ring: vazifalar ilovasidagi Vazifa klassi nusxalarini structuredClone bilan nusxalasangiz, nima bo'ladi? Klassda private # maydon bo'lsa-chi?

Javob

Nusxalar oddiy obyektlarga aylanadi: instanceof Vazifa — false, metodlar yo'q. Private maydonlar esa umuman nusxalanmaydi — ular xususiyat emas. Shuning uchun vazifalar da holat paket.js orqali oddiy ma'lumotga aylantirilib saqlanadi va qayta Vazifa.dan(...) bilan klassga o'raladi.

6. Structural sharing chuqur

6.1 Faqat yo'l nusxalanadi

Immutable yangilash darsida tuzilmani bo'lishish (structural sharing) atamasini kiritgan edik: yangi holat eskisining o'zgarmagan qismlarini qayta ishlatadi. Endi uni katta ro'yxatda ko'ramiz.

vazifalar da 10 000 ta vazifa bor. 5001-vazifa bajarildi deb belgilanadi. Immutable yangilash nimani yangidan yasaydi?

js
const vazifalar = Array.from({ length: 10000 }, (_, i) => ({
  id: i + 1,
  matn: `Vazifa ${i + 1}`,
  bajarildi: false,
}));

const yangi = vazifalar.with(5000, {
  ...vazifalar[5000],
  bajarildi: true,
});

const umumiylar = yangi.filter((v, i) => v === vazifalar[i]).length;

console.log(yangi === vazifalar); // false
console.log(umumiylar); // 9999
console.log(vazifalar[5000].bajarildi); // false

Ikki yangi narsa yasaldi: massiv va bitta vazifa obyekti. Qolgan 9 999 ta vazifa — eski va yangi massivda ayni bir obyektlar (===). Sxemada:

flowchart TB
  E["eski massiv"] --> V1["vazifa 1"]
  E --> V2["... 9 998 ta ..."]
  E --> VE["vazifa 5001<br/>bajarildi: false"]
  Y["yangi massiv"] --> V1
  Y --> V2
  Y --> VY["vazifa 5001<br/>bajarildi: true"]

Massivning o'zi baribir yangidan yasaldi — lekin u faqat 10 000 ta havola saqlaydi, vazifalarning o'zini emas. Havolani nusxalash — vazifani nusxalashdan ancha arzon.

6.2 O'lchov: qanchalik arzon?

Xuddi shu yangilashni uch usulda o'lchadik: with (bitta indeksni almashtirish), map (hamma elementni ko'rib, keraklisini almashtirish) va structuredClone (hammasini nusxalab, keyin o'zgartirish). O'lchovda har vazifaga to'rtinchi maydon — teglar massivi ham qo'shildi:

10 000 vazifali ro'yxatda bitta vazifani yangilash
  • with + spreadmassiv + 1 obyekt0,007 ms
  • map + spread10 000 tekshiruv0,093 ms
  • structuredClone20 000+ obyekt nusxasi10,9 ms

Manba: O'lchov: Node 24.21, Windows 11, Intel Core i5-12500H, performance.now, 100 yangilashning o'rtachasi, 9 o'lchovning medianasi, 2026-10-05

structuredClone with dan taxminan 1 500 barobar sekinroq. Sababi aniq: u 10 000 vazifa, 10 000 teglar massivi va massivning o'zini — 20 000 dan ortiq obyektni yangidan yasaydi. with esa bitta massiv va bitta obyekt. Sizning kompyuteringizda raqamlar boshqacha bo'ladi, lekin farqning tartibi shunday qoladi.

Boshqa tomondan qarang: 0,1 ms — bu map uchun. Foydalanuvchi bosgan tugmaga javob uchun bu sezilmaydi. Shuning uchun vazifalar kabi ilovalarda oddiy map + spread to'liq yetarli. structuredClone esa ilova holatini har bosishda yangilash uchun emas.

6.3 Juda katta ma'lumot: maxsus tuzilmalar

with ham massivni to'liq nusxalaydi — 10 000 havolani. Million elementli ro'yxatda bu ham sezila boshlaydi. Buning uchun doimiy tuzilmalar (persistent data structures) bor: ro'yxatni kichik bo'laklardan iborat daraxt qilib saqlaydi va yangilashda faqat bitta shoxni nusxalaydi. Ro'yxat o'n barobar o'ssa, ish deyarli o'zgarmaydi. Ular Immutable.js (5.1 versiya) kutubxonasida bor. Bunday "o'sish tezligi"ni o'lchash tili — Big-O — 14-qismda. Oddiy ilovalarda bu kerak bo'lmaydi.

Til darajasida ham urinish bo'lgan: Records & Tuples taklifi #{ ... } va #[ ... ] ko'rinishidagi chuqur o'zgarmas qiymatlarni qo'shmoqchi edi. 2025-yil aprelida TC39 (JavaScript standartini yozadigan qo'mita) yig'ilishida mualliflar bu taklifni qaytarib oldi. Uning GitHub sahifasi arxivlandi. G'oyaning bir qismi yangi, ancha kichik Composites taklifida davom etmoqda (hozir dastlabki, 1-bosqichda). Demak, bugun immutability — til kafolati emas, dasturchi kelishuvi va vositalar.

7. Immer g'oyasi

7.1 Muammo: uch qavatli spread

Ichma-ich holatni yangilash to'g'ri, lekin o'qishga og'ir:

js
const yangi = {
  ...holat,
  buyurtma: {
    ...holat.buyurtma,
    taomlar: holat.buyurtma.taomlar.map((t) =>
      t.id === 2 ? { ...t, soni: t.soni + 1 } : t,
    ),
  },
};

Niyat oddiy — "2-taomning sonini bittaga oshir". Mutatsiya bilan bu bitta qator bo'lardi.

7.2 Immer: "joyida yozing, o'zgarmas oling"

Immer (hozir 11.1 versiya) — shu muammoni hal qiladigan kutubxona. Uning asosiy funksiyasi produce. Siz unga holat va "retsept" funksiyasini berasiz. Retsept parametr sifatida qoralama (draft) oladi — holatga o'xshagan, lekin "qoralama daftar" kabi vaqtinchalik nusxa. Siz qoralamani joyida o'zgartirgandek yozasiz. Natija — yangi, o'zgarmas holat, asl esa tegilmaydi:

js
import { produce } from "immer";

const holat = {
  filtr: "hammasi",
  vazifalar: [
    { id: 1, matn: "Non olish", bajarildi: false },
    { id: 2, matn: "Choy damlash", bajarildi: false },
  ],
};

const yangi = produce(holat, (qoralama) => {
  qoralama.vazifalar[0].bajarildi = true;
});

const eskisi = holat.vazifalar[0].bajarildi;
console.log(eskisi, yangi.vazifalar[0].bajarildi);
console.log(yangi.vazifalar[1] === holat.vazifalar[1]);
console.log(Object.isFrozen(yangi));

Scratch papkada npm i immer@11.1.21 bilan Node 24 da ishga tushirildi:

text
false true
true
true

Uch qatorni o'qing:

  • asl holat false bo'lib qoldi, yangisida true — mutatsiya asl obyektga tegmadi;
  • 2-vazifa eski va yangi holatda bitta obyekt — Immer structural sharing'ni o'zi qildi;
  • natija muzlatilgan — keyin kimdir uni tasodifan o'zgartirsa, qat'iy rejimda TypeError chiqadi.

Retsept hech narsani o'zgartirmasa, produce asl holatning o'zini qaytaradi — === tekshiruvi "o'zgarish yo'q" deydi.

Kutubxona ulash va npm paketlari bilan ishlashni 16-qismda chuqur o'rganamiz. Hozir g'oya muhim.

7.3 Ichida nima bor? Proxy!

qoralama — asl holat emas. Bu Proxy. Retsept qoralama.vazifalar[0].bajarildi = true deb yozganda, proxy'ning set trap'i ishlaydi. U asl obyektga yozmaydi — o'zgarishni eslab qoladi va o'zgargan yo'ldagi obyektlarni "nusxalangan" deb belgilaydi. Retsept tugagach, Immer faqat belgilangan yo'lni nusxalab, qolganini asldan oladi.

Ya'ni Immer — o'tgan uch darsning birikmasi: Proxy (ushlab qolish), FP (asl ma'lumotga tegmaslik) va structural sharing (arzon nusxa). Shunga o'xshash mini-versiyani o'zingiz 3-mashqda yozasiz.

Redux Toolkit (Redux'ning rasmiy to'plami) ichida Immer ishlatadi. Shuning uchun Redux Toolkit kodida state.soni++ kabi "mutatsiya" ko'rsangiz, qo'rqmang — u Immer'ning qoralamasi. React va Redux'ni keyingi qismlarda o'rganamiz.

Tekshirib ko'ring: Retsept hech narsani o'zgartirmasa — produce(holat, (q) => {}) — natija === holat bo'ladimi? Bu nega foydali?

Javob

Ha, true. O'zgarish bo'lmasa, Immer yangi obyekt yasamaydi va aslning o'zini qaytaradi. Shunda «O'zgarishni bir zumda sezish» bo'limidagi === tekshiruvi "o'zgarish yo'q" deydi va ekran keraksiz qayta chizilmaydi.

8. Ko'p uchraydigan xatolar

8.1 freeze ni chuqur deb o'ylash

Object.freeze(holat) dan keyin holat.buyurtma.taomlar.push(...) bemalol ishlaydi. Tuzatish: chuqur muzlatish funksiyasi (freeze darsidagi chuqurMuzlat) yoki Immer.

8.2 Muzlagan Map ga ishonish

Object.isFrozen(map) — true, lekin map.set() ishlaydi. Tuzatish: Map, Set, Date ni immutable naqsh bilan yangilang: new Map(eski).set(k, v).

8.3 Klass nusxasini structuredClone qilish

Xato chiqmaydi, lekin metodlar yo'qoladi va keyinroq TypeError: nusxa.chegirma is not a function chiqadi. Tuzatish: klassning o'z nusxalash metodi yoki konstruktor orqali yangi nusxa.

8.4 Holatni har safar structuredClone qilish

"Xavfsiz bo'lsin" deb har o'zgarishda butun holatni nusxalash — sekin va === tekshiruvini buzadi: hamma narsa "o'zgargan" bo'lib ko'rinadi. Tuzatish: faqat o'zgargan yo'lni nusxalang.

8.5 Retsept ichida qaytarish va o'zgartirishni aralashtirish

Immer retseptida yoki qoralamani o'zgartiring, yoki butunlay yangi qiymat qaytaring — ikkalasini birga emas. Ikkalasi birga bo'lsa, Immer xato beradi:

text
Error: [Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.

Tarjimasi: "Immer retsepti yangi qiymat qaytardi va qoralamani o'zgartirdi. Yo yangi qiymat qaytaring, yo qoralamani o'zgartiring." Ko'pincha bu ataylab emas, tasodifan bo'ladi: (q) => q.soni++ — figurali qavssiz arrow q.soni++ ning qiymatini qaytaradi. Tuzatish: arrow funksiyada figurali qavs qo'ying: (q) => { q.soni++; }, aks holda q.soni++ ning qiymati qaytib ketadi.

9. Mashqlar

1-mashq (oson): Kalitni o'chirish va Map yangilash

Ikki funksiya yozing. ichkiniOlibTashla(mehmon) — _ bilan boshlanadigan bitta _id kalitisiz yangi obyekt qaytarsin. narxniYangila(narxlar, taom, narx) — Map ning yangi nusxasini qaytarsin. Asl qiymatlar o'zgarmasin. Ishora: destructuring rest va new Map(...).

js
const mehmon = { _id: 7, ism: "Malika" };
console.log(ichkiniOlibTashla(mehmon)); // { ism: 'Malika' }
Yechim
js
function ichkiniOlibTashla(mehmon) {
  const { _id, ...qolgan } = mehmon;
  return qolgan;
}

function narxniYangila(narxlar, taom, narx) {
  return new Map(narxlar).set(taom, narx);
}

const mehmon = { _id: 7, ism: "Malika" };
console.log(ichkiniOlibTashla(mehmon)); // { ism: 'Malika' }
console.log(mehmon._id); // 7

const narxlar = new Map([["osh", 35000]]);
const yangi = narxniYangila(narxlar, "osh", 38000);
console.log(narxlar.get("osh"), yangi.get("osh")); // 35000 38000

_id o'zgaruvchisi ishlatilmaydi — u faqat kalitni "chetga olish" uchun. Linter bunday o'zgaruvchi haqida ogohlantirishi mumkin. _ bilan boshlangan nom — "ataylab ishlatilmaydi" degan kelishuv.

2-mashq (o'rta): Bekor qilish va qaytarish

tarixYarat(boshlangich) funksiyasini yozing. U obyekt qaytarsin: hozir() — joriy holat, ozgartir(yangiHolat) — yangi holatni qo'shadi, bekorQil() — bir qadam orqaga, qaytar() — bir qadam oldinga. Holatlar massivda va joriy indeksda saqlansin (closure bilan). Yangi o'zgarish "qaytar" yo'lini kesib tashlasin. Ishora: slice(0, indeks + 1) va spread.

js
const t = tarixYarat({ taomlar: [] });
t.ozgartir({ taomlar: ["Osh"] });
t.ozgartir({ taomlar: ["Osh", "Manti"] });
t.bekorQil();
console.log(t.hozir()); // { taomlar: [ 'Osh' ] }
Yechim
js
function tarixYarat(boshlangich) {
  let holatlar = [boshlangich];
  let indeks = 0;
  return {
    hozir: () => holatlar[indeks],
    ozgartir(yangiHolat) {
      holatlar = [...holatlar.slice(0, indeks + 1), yangiHolat];
      indeks = holatlar.length - 1;
    },
    bekorQil() {
      if (indeks > 0) indeks--;
    },
    qaytar() {
      if (indeks < holatlar.length - 1) indeks++;
    },
  };
}

const t = tarixYarat({ taomlar: [] });
t.ozgartir({ taomlar: ["Osh"] });
t.ozgartir({ taomlar: ["Osh", "Manti"] });
t.bekorQil();
console.log(t.hozir()); // { taomlar: [ 'Osh' ] }
t.qaytar();
console.log(t.hozir()); // { taomlar: [ 'Osh', 'Manti' ] }
t.bekorQil();
t.ozgartir({ taomlar: ["Osh", "Choy"] });
t.qaytar();
console.log(t.hozir()); // { taomlar: [ 'Osh', 'Choy' ] }

Oxirgi qadamga qarang: "Manti" li holatdan bekor qilib, yangi o'zgarish qildik — "qaytar" yo'li kesildi va qaytar() hech narsa qilmadi. Matn muharrirlaridagi Ctrl+Z / Ctrl+Y xuddi shunday ishlaydi. Bu tarix faqat holatlar immutable bo'lsa ishlaydi: kimdir t.hozir().taomlar.push(...) qilsa, tarixdagi holat buziladi.

3-mashq (qiyin): Mini-Immer

yasa(asl, retsept) funksiyasini (Immer'ning produce i kabi) yozing (faqat bir qavatli obyektlar uchun). U asl ni Proxy ga o'rasin: set trap'i asl obyektga emas, alohida ozgarishlar obyektiga yozsin; get trap'i avval ozgarishlar ga, keyin aslga qarasin. Retsept tugagach: haqiqiy o'zgarish bo'lmasa — asl ning o'zini qaytaring, bo'lsa — muzlatilgan yangi obyektni. Ishora: Object.hasOwn, Object.is va Reflect.get (Proxy va Reflect).

js
const sozlama = { ochilish: "07:00", yopilish: "23:00" };
const yangi = yasa(sozlama, (q) => { q.yopilish = "00:00"; });
console.log(yangi); // { ochilish: '07:00', yopilish: '00:00' }
console.log(sozlama.yopilish); // 23:00
Yechim
js
function yasa(asl, retsept) {
  const ozgarishlar = {};
  const qoralama = new Proxy(asl, {
    get(nishon, kalit, receiver) {
      if (Object.hasOwn(ozgarishlar, kalit)) {
        return ozgarishlar[kalit];
      }
      return Reflect.get(nishon, kalit, receiver);
    },
    set(nishon, kalit, qiymat) {
      ozgarishlar[kalit] = qiymat;
      return true;
    },
  });

  retsept(qoralama);

  const haqiqiy = Object.keys(ozgarishlar).filter(
    (kalit) => !Object.is(ozgarishlar[kalit], asl[kalit]),
  );
  if (haqiqiy.length === 0) return asl;
  return Object.freeze({ ...asl, ...ozgarishlar });
}

const sozlama = { ochilish: "07:00", yopilish: "23:00" };

const yangi = yasa(sozlama, (q) => {
  q.yopilish = "00:00";
});
console.log(yangi, sozlama.yopilish);

const xuddiOzi = yasa(sozlama, (q) => {
  q.ochilish = "07:00";
});
const ozimi = xuddiOzi === sozlama;
console.log(ozimi, Object.isFrozen(yangi)); // true true

Konsolda:

text
{ ochilish: '07:00', yopilish: '00:00' } 23:00
true true

Ikkinchi chaqiruvda retsept qiymatni o'sha qiymatga "o'zgartirdi" — Object.is buni sezdi va asl obyekt qaytdi. Shunday qilib === tekshiruvi "o'zgarish yo'q" deydi va keraksiz chizish bo'lmaydi. Haqiqiy Immer xuddi shu g'oyani ichma-ich obyektlar, massivlar, Map va Set uchun qiladi: har ichki obyekt o'qilganda uni ham proxy'ga o'raydi.

4-mashq: Vazifalar qadami — tarix va umumiy qismlar

Mashqlar repoda kurs/mashqlar/12/23-immutability/tarix.mjs yarating. 2-mashqdagi tarixYarat ni vazifalar ro'yxati bilan sinang: boshlang'ich ro'yxatga yangi vazifa qo'shing, keyin bitta vazifani bajarilgan deb belgilang — ikkalasini immutable ([...royxat, yangi] va map + spread). Har qadamdan keyin oldingi holat bilan nechta vazifa umumiy (===) ekanini chiqaring. Oxirida bekorQil qilib, eski holat buzilmaganini tekshiring.

Yechim
js
function tarixYarat(boshlangich) {
  let holatlar = [boshlangich];
  let indeks = 0;
  return {
    hozir: () => holatlar[indeks],
    ozgartir(yangiHolat) {
      holatlar = [...holatlar.slice(0, indeks + 1), yangiHolat];
      indeks = holatlar.length - 1;
    },
    bekorQil() {
      if (indeks > 0) indeks--;
    },
  };
}

const umumiySoni = (a, b) => b.filter((v) => a.includes(v)).length;

const tarix = tarixYarat([
  { id: 1, matn: "Non olish", bajarildi: false },
  { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
]);

const r0 = tarix.hozir();
const yangiVazifa = { id: 3, matn: "Choy damlash", bajarildi: false };
tarix.ozgartir([...r0, yangiVazifa]);
const r1 = tarix.hozir();
console.log("qo'shish:", umumiySoni(r0, r1), "umumiy");

tarix.ozgartir(
  r1.map((v) => (v.id === 1 ? { ...v, bajarildi: true } : v)),
);
const r2 = tarix.hozir();
console.log("belgilash:", umumiySoni(r1, r2), "umumiy");

tarix.bekorQil();
console.log(tarix.hozir() === r1, r1[0].bajarildi); // true false

Konsolda:

text
qo'shish: 2 umumiy
belgilash: 2 umumiy
true false

Qo'shishda ikkala eski vazifa yangi ro'yxatda o'sha obyektlar bo'lib qoldi. Belgilashda uchtadan ikkitasi umumiy — faqat 1-vazifa yangidan yasaldi. bekorQil dan keyin r1 ning birinchi vazifasi hamon false — tarix buzilmagan. Terminalda: node 12/23-immutability/tarix.mjs.

bash
git add 12/23-immutability
git commit -m "12/23: immutable tarix va umumiy qismlar"

10. Real ishda

  • React. setState va useReducer holatni === bilan solishtiradi. Joyida o'zgartirilgan holat — ekranda yangilanmagan UI: React'dagi eng ko'p uchraydigan boshlovchi xatosi.
  • Redux Toolkit va Zustand. Ikkalasi ham holatni immutable deb hisoblaydi; Redux Toolkit ichida Immer ishlaydi, Zustand'da u ixtiyoriy qo'shimcha.
  • Bekor qilish/qaytarish. Figma, Google Docs, matn muharrirlari va o'yinlarda tarix — holatlar ketma-ketligi. Immutability'siz uni ishonchli qurish qiyin.
  • Kesh va memoization. "Kirish o'zgarmagan bo'lsa — eski natijani ber" — === bilan arzon tekshiriladi. Memoization darsida shunga tayanamiz.
  • Intervyu. "Object.freeze chuqur ishlaydimi?", "structuredClone nimalarni nusxalamaydi?", "Structural sharing nima?", "Immer qanday ishlaydi?" — middle darajadagi savollar.

Xulosa

  • Immutability foydalari: === bilan tez o'zgarish aniqlash, bepul tarix va "bekor qilish", kutilmagan o'zgarishdan va asinxron poygadan himoya, oson debug; narxi — ko'proq obyekt va yozuv.
  • Naqshlar: { ...obj, x }, const { x, ...qolgan } = obj, with/toSpliced, new Map(eski).set(), new Set(eski).add().
  • Object.freeze — sayoz; Map, Set, Date va # maydonlarni to'xtatmaydi; typed array'ni umuman muzlatmaydi.
  • structuredClone — funksiyada xato, klassni oddiy obyektga aylantiradi, symbol kalit va freeze ni yo'qotadi; ilova holatini yangilash uchun emas.
  • Structural sharing faqat o'zgargan yo'lni nusxalaydi: 10 000 vazifada with — 0,007 ms, structuredClone — 11 ms.
  • Immer Proxy qoralamasi bilan "joyida yozing — o'zgarmas oling" imkonini beradi va structural sharing'ni o'zi qiladi.

Keyingi dars: Kompozitsiya: compose va pipe — toza, o'zgarmas ma'lumot bilan ishlaydigan kichik funksiyalarni bitta katta funksiyaga ulash.

Manbalar

  • MDN: "Object.freeze()", "structuredClone()", "The structured clone algorithm" — developer.mozilla.org
  • Immer: "Introduction to Immer", "Pitfalls" — immerjs.github.io/immer
  • Redux: "Immutable Update Patterns" — redux.js.org/usage/structuring-reducers/immutable-update-patterns
  • TC39: "Record & Tuple" taklifi (qaytarib olingan, 2025-04-14 yig'ilishi; issue #394) — github.com/tc39/proposal-record-tuple; "Composites" (1-bosqich) — github.com/tc39/proposal-composites; tc39/proposals, "Inactive proposals" ro'yxati (2026-10-05 da tekshirildi)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript immutability chuqur: Object.freeze, structuredClone va structural sharing — IlmHamroh