IlmHamroh
JavaScript Full-stack/10-qism. JavaScript chuqur16/65-dars22 daqiqa
Mundarija (32)

JavaScript'da immutable yangilash: ichma-ich obyekt va massivni asl nusxaga tegmasdan o'zgartirish

Qisqacha: Immutable yangilash — ma'lumotni joyida o'zgartirmay, o'zgargan yangi nusxa yasash. Ichma-ich obyektda o'zgargan qiymatgacha bo'lgan yo'ldagi har bir obyekt spread bilan nusxalanadi: { ...h, mijoz: { ...h.mijoz, ism: "Ali" } }, qolgan hammasi esa eski nusxa bilan umumiy qoladi. Massiv ichidagi obyekt — map yoki with bilan, qo'shish — [...arr, x], o'chirish — filter.

Bu darsda

  • Nega ma'lumotni joyida o'zgartirish React kabi vositalarda (interfeys yasash kutubxonasi, 17-qismda o'rganamiz) xato beradi — tushuntira olasiz.
  • Ichma-ich obyektni yo'l bo'ylab nusxa usuli bilan yangilay olasiz.
  • Massiv ichidagi obyektni map va with bilan, qo'shish va o'chirishni nusxa bilan qila olasiz.
  • ?. bilan chuqur qiymatni xavfsiz o'qiy olasiz va nega u tayinlashda ishlamasligini bilasiz.
  • vazifalar ni muzlagan ma'lumot bilan ishlaydigan qilib qayta yozasiz va obyektlar modulini yakunlaysiz.

Oldin bilishingiz kerak: Object.* metodlari, Nusxa bilan o'zgartirish: toSorted, with, Sayoz va chuqur nusxalash, map, ?? va ?., Obyektni himoyalash.

1. Nega bu kerak?

Sayoz va chuqur nusxalash darsida bir naqshni ko'rgan edik: o'zgaradigan obyektni nusxalab, yangi qiymat bilan almashtirish. O'shanda "to'liq amaliyot — keyinroq" degan edik. Bugun shu va'dani bajaramiz.

Avval — nega aynan nusxa? «Bahor»dagi bron obyekti:

js
const bron = {
  mijoz: { ism: "Malika", manzil: { shahar: "Toshkent", uy: 7 } },
  taomlar: [{ id: 1, nom: "Osh", soni: 1 }],
};

const kechagi = bron;
bron.mijoz.manzil.uy = 12;

console.log(kechagi === bron); // true
console.log(kechagi.mijoz.manzil.uy); // 12

Biz "kechagi holat"ni saqlab qo'ydik, keyin uy raqamini o'zgartirdik. Natija: kechagi holat ham o'zgardi! kechagi va bron — bitta obyekt (Havola semantikasi). Endi ikki muammo bor:

  1. O'zgarishni sezib bo'lmaydi. "Holat o'zgardimi?" degan savolga kechagi === bron "yo'q" deydi. React (17-qismda o'rganadigan kutubxona; hozir bilish shart emas) aynan shunday solishtiradi — Object.is bilan (Object.* metodlari). Joyida o'zgartirilgan holatda havola o'sha, demak React ekranni yangilamaydi.
  2. Tarix yo'qoladi. "Bekor qilish" (undo) tugmasi uchun eski holat kerak. Lekin eski holat yo'q — u yangisi bilan bitta obyekt.

Hayotdan o'xshatish: kassadagi chek. Otabek xato chek urdi. U eski chekni qog'ozdan o'chirib, ustiga yozmaydi. Yangi, to'g'ri chek uriladi, eskisi esa "bekor qilindi" deb tarixda qoladi. Buxgalter keyin ikkalasini ham ko'radi. Immutable yangilash — xuddi shunday: eski holat joyida qoladi, yangisi alohida yasaladi.

Immutable yangilash — bu atama bilan Nusxa bilan o'zgartirish darsida tanishgan edik: ma'lumotni o'zgartirish o'rniga, o'zgarishi kiritilgan yangi nusxasini yasash. U yerda — tekis massiv, bugun — ichma-ich obyektlar.

2. Ichma-ich obyektni yangilash

2.1 Sayoz spread yetmaydi

Birinchi urinish — bitta spread:

js
const bron = {
  mijoz: { ism: "Malika", manzil: { shahar: "Toshkent", uy: 7 } },
};

const yangi = { ...bron };
yangi.mijoz.manzil.uy = 12;

console.log(bron.mijoz.manzil.uy); // 12

Yana buzildi. { ...bron } faqat birinchi qavatni nusxaladi. yangi.mijoz — eski bron.mijoz ning o'zi. Ichiga yozish — eskisiga yozish.

2.2 Yo'l bo'ylab nusxa

To'g'ri usul — o'zgaradigan qiymatgacha bo'lgan yo'ldagi har bir obyektni nusxalash. uy gacha yo'l: bron → mijoz → manzil → uy. Demak uchta spread kerak:

js
const bron = {
  mijoz: { ism: "Malika", manzil: { shahar: "Toshkent", uy: 7 } },
  taomlar: [{ id: 1, nom: "Osh", soni: 1 }],
};

const yangi = {
  ...bron,
  mijoz: {
    ...bron.mijoz,
    manzil: { ...bron.mijoz.manzil, uy: 12 },
  },
};

console.log(bron.mijoz.manzil.uy, yangi.mijoz.manzil.uy); // 7 12
console.log(yangi === bron); // false
console.log(yangi.mijoz === bron.mijoz); // false
console.log(yangi.taomlar === bron.taomlar); // true

Ichkaridan o'qing:

  1. { ...bron.mijoz.manzil, uy: 12 } — manzilning nusxasi, uy almashtirilgan.
  2. { ...bron.mijoz, manzil: ... } — mijozning nusxasi, manzil yangisiga almashtirilgan.
  3. { ...bron, mijoz: ... } — bronning nusxasi, mijoz yangisiga almashtirilgan.

Oxirgi qatorga qarang: yangi.taomlar === bron.taomlar — true. Taomlar o'zgarmadi, shuning uchun nusxalanmadi — ikkala bron bitta massivni bo'lishadi. Bu xato emas, aynan maqsad: o'zgarmagan qism ortiqcha nusxalanmaydi.

Sxemada kuzating — yangi bron qaysi qismlarni yangidan yasadi va qaysilarini eskisidan oldi:

flowchart TB
  E["eski bron"] --> EM["eski mijoz"] --> EA["eski manzil: uy 7"]
  Y["yangi bron"] --> YM["yangi mijoz"] --> YA["yangi manzil: uy 12"]
  E --> T["taomlar (umumiy)"]
  Y --> T

Yo'ldagi uchta obyekt — yangi, yo'ldan tashqaridagi taomlar — umumiy. Bu usulning nomi — tuzilmani bo'lishish (structural sharing): yangi holat eski holatning o'zgarmagan qismlarini qayta ishlatadi. Shuning uchun immutable yangilash katta ma'lumotda ham arzon: ming taomli buyurtmada uy raqamini o'zgartirish uchun ming taom nusxalanmaydi.

Tekshirib ko'ring: Yuqoridagi bron da faqat mijoz ismini "Malika Karimova" ga o'zgartirish uchun nechta spread kerak? yangi.mijoz.manzil === bron.mijoz.manzil bo'ladimi?

Javob

Ikkita: { ...bron, mijoz: { ...bron.mijoz, ism: "Malika Karimova" } }. Yo'l — bron → mijoz → ism, ism esa satr (primitiv, nusxa kerak emas). manzil yo'lda emas, shuning uchun u umumiy qoladi: true.

2.3 structuredClone emasmi?

"Butunini chuqur nusxalab, keyin bemalol o'zgartiraman" — ham yo'l:

js
const bron = {
  mijoz: { ism: "Malika", manzil: { uy: 7 } },
  taomlar: [{ id: 1, nom: "Osh", soni: 1 }],
};

const yangi = structuredClone(bron);
yangi.mijoz.manzil.uy = 12;

console.log(bron.mijoz.manzil.uy); // 7
console.log(yangi.taomlar === bron.taomlar); // false

Ishladi — asl o'zgarmadi. Lekin taomlar ham nusxalandi, garchi u o'zgarmagan bo'lsa ham. Bu ikki narsaga olib keladi:

Yo'l bo'ylab spread structuredClone + o'zgartirish
Nima nusxalanadi faqat yo'ldagi obyektlar hamma narsa
O'zgarmagan qism umumiy (=== — true) yangi (=== — false)
Katta ma'lumotda arzon qimmat
"Nima o'zgardi?" savoli === bilan aniq javob hamma narsa "o'zgargan" ko'rinadi

React kabi vositalar === bilan "bu qism o'zgardimi?" deb so'raydi. structuredClone dan keyin hamma qism "o'zgargan" bo'ladi va hamma narsa qayta chiziladi. Shuning uchun ilova holatida asosiy usul — yo'l bo'ylab spread. structuredClone esa — ma'lumotni butunlay mustaqil qilish kerak bo'lganda (masalan, "nusxa olish" tugmasi).

2.4 Yangilash — toza funksiya

Immutable yangilashni funksiyaga o'rasak, u alohida xususiyatga ega bo'ladi: eski holat kiradi, yangi holat chiqadi, tashqarida hech narsa o'zgarmaydi. Bu — Pure funksiya darsidagi toza funksiya:

js
function uyniAlmashtir(bron, uy) {
  return {
    ...bron,
    mijoz: {
      ...bron.mijoz,
      manzil: { ...bron.mijoz.manzil, uy },
    },
  };
}

const b0 = { mijoz: { ism: "Malika", manzil: { uy: 7 } } };
const b1 = uyniAlmashtir(b0, 12);
const b2 = uyniAlmashtir(b1, 15);

const uylar = [b0, b1, b2].map((b) => b.mijoz.manzil.uy);
console.log(...uylar);

Konsolda:

text
7 12 15

Uch holat — uchalasi ham tirik. b0 — boshlang'ich, b1 — birinchi o'zgarishdan keyin, b2 — ikkinchisidan keyin. Hech biri boshqasini buzmadi. Bunday funksiyani sinash oson: bir xil kirishga doim bir xil natija, va natijani eskisi bilan solishtirish mumkin.

Real loyihalarda holatni boshqaradigan butun qism shunday funksiyalardan quriladi: "eski holat + amal → yangi holat". Bu naqsh reducer deb ataladi — nomi reduce dagi akkumulyatordan: har amal eski holatdan yangisini "yig'adi". Redux va React'ning useReducer i aynan shu g'oyaga qurilgan — ularni keyingi qismlarda ko'rasiz.

3. Massivlarni immutable yangilash

3.1 Asosiy amallar jadvali

Massivlar uchun asboblar allaqachon bor — map, filter va Nusxa bilan o'zgartirish darslaridan. Ularni bitta jadvalga yig'amiz:

Amal Joyida ( holatda) Nusxa bilan ()
Oxiriga qo'shish arr.push(x) [...arr, x]
Boshiga qo'shish arr.unshift(x) [x, ...arr]
O'chirish arr.splice(i, 1) arr.filter(...), arr.toSpliced(i, 1)
Almashtirish arr[i] = x arr.with(i, x)
Tartiblash arr.sort(...) arr.toSorted(...)
Har birini o'zgartirish forEach ichida yozish arr.map(...)

Chap ustundagilar holat uchun xavfli — ular massivning o'zini o'zgartiradi. O'ng ustundagilar har doim yangi massiv qaytaradi.

Tekshirib ko'ring: Ro'yxatning o'rtasiga — 1-indeksga — yangi element qo'shish kerak. Jadvalda yo'q. Qaysi nusxa metodi bilan qilasiz?

Javob

arr.toSpliced(1, 0, x) — 1-indeksdan boshlab 0 ta element o'chiradi va x ni qo'yadi (Nusxa bilan o'zgartirish). Spread bilan ham bo'ladi: [...arr.slice(0, 1), x, ...arr.slice(1)] — lekin toSpliced niyatni aniqroq aytadi.

3.2 Massiv ichidagi obyekt

Eng ko'p uchraydigan holat: ro'yxatdagi bitta obyektni yangilash. Buyurtmada osh sonini bittaga oshiramiz. map darsidagi naqsh:

js
const taomlar = [
  { id: 1, nom: "Osh", soni: 1 },
  { id: 2, nom: "Ko'k choy", soni: 2 },
];

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

console.log(yangi[0].soni, taomlar[0].soni); // 2 1
console.log(yangi[1] === taomlar[1]); // true

map har elementga qaraydi: kerakli id bo'lsa — yangi obyekt ({ ...t, soni: ... }), aks holda — o'sha obyekt. Choy o'zgarmadi — u ikkala massivda umumiy. Bu ham yo'l bo'ylab nusxa: massiv → bitta obyekt.

Indeks ma'lum bo'lsa, with qisqaroq:

js
const taomlar = [
  { id: 1, nom: "Osh", soni: 1 },
  { id: 2, nom: "Ko'k choy", soni: 2 },
];

const i = taomlar.findIndex((t) => t.id === 2);
const yangi = taomlar.with(i, { ...taomlar[i], soni: 3 });

console.log(yangi[1].soni, taomlar[1].soni); // 3 2

Ikkalasi ham to'g'ri. map — "id bo'yicha" fikrlashga yaqin va element topilmasa ham xatosiz ishlaydi. with — indeks bilan, lekin findIndex -1 qaytarsa, with(-1, ...) oxirgi elementni almashtiradi (Nusxa bilan o'zgartirish) — bunday holatni oldindan tekshiring.

3.3 Uch qavat: massiv ichidagi obyekt ichidagi massiv

Har taomning qo'shimchalari bor. Oshga "qo'shimcha go'sht" qo'shamiz:

js
const taomlar = [
  { id: 1, nom: "Osh", qoshimcha: ["salat"] },
  { id: 2, nom: "Manti", qoshimcha: [] },
];

const yangi = taomlar.map((t) =>
  t.id === 1 ? { ...t, qoshimcha: [...t.qoshimcha, "go'sht"] } : t,
);

console.log(yangi[0].qoshimcha); // [ 'salat', "go'sht" ]
console.log(taomlar[0].qoshimcha); // [ 'salat' ]

Yo'l: massiv (map) → obyekt ({ ...t }) → ichki massiv ([...t.qoshimcha, ...]). Qoida o'sha: yo'ldagi har qavat — nusxa, qolgani — umumiy. Qavatlar ko'paygan sari kod uzayadi — buni «Real ishda» bo'limida eslaymiz.

Tekshirib ko'ring: taomlar.map((t) => { t.soni++; return t; }) — immutable yangilashmi? Natija yangi massivmi?

Javob

Massiv yangi, lekin obyektlar — eskilari, va ular o'zgartirildi (t.soni++). Bu immutable emas: asl taomlar dagi obyektlar ham buzildi. map o'zi xavfsiz, lekin callback ichida mutatsiya qilinsa — himoya yo'q (Callback imzosi darsidagi qoida). To'g'risi: (t) => ({ ...t, soni: t.soni + 1 }).

3.4 Lug'at ko'rinishidagi obyekt

reduce darsida ro'yxatni id bo'yicha "lug'at" ga aylantirgan edik: { 1: {...}, 3: {...} }. Bunday obyektda bitta yozuvni yangilash — kalit nomi o'zgaruvchida, shuning uchun computed kalit kerak:

js
const taomlar = {
  1: { nom: "Osh", soni: 1 },
  2: { nom: "Manti", soni: 2 },
};

const id = 2;
const yangi = { ...taomlar, [id]: { ...taomlar[id], soni: 3 } };

console.log(yangi[2].soni, taomlar[2].soni); // 3 2
console.log(yangi[1] === taomlar[1]); // true

[id]: ... — kalit nomi id o'zgaruvchisidan olinadi. Yo'l: lug'at → bitta yozuv; yozuvdan tashqaridagi osh umumiy qoldi. Lug'atdan o'chirish ham nusxa bilan: obyekt destructuring dagi rest — const { [id]: _, ...qolgan } = taomlar; — _ — "bu qiymat kerak emas" degani uchun odatiy nom (o'chirilayotgan yozuv shunga tushadi). qolgan da id kaliti yo'q, asl lug'at esa joyida. Massiv bilanmi yoki lug'at bilanmi ishlash — vazifaga bog'liq; immutable yangilash qoidasi ikkalasida bir xil.

4. ?. — chuqur qiymatni xavfsiz o'qish

4.1 O'qishda

Ichma-ich ma'lumotda ba'zi qavatlar bo'lmasligi mumkin: mehmon manzilsiz bron qilgan. ?? va ?. darsidagi ?. chuqur o'qishni xavfsiz qiladi:

js
const bron = { mijoz: { ism: "Bobur" } };

const uy = bron.mijoz?.manzil?.uy ?? "ko'rsatilmagan";
console.log(uy); // ko'rsatilmagan

bron.mijoz.manzil.uy — TypeError: Cannot read properties of undefined (reading 'uy') berardi. ?. esa manzil yo'qligini ko'rib, darhol undefined qaytardi, ?? uni tushunarli matnga aylantirdi.

Yangilashda ham bu muhim: yo'q qavatni spread qilish xato bermaydi. { ...undefined } — bo'sh obyekt. Shuning uchun manzili yo'q mehmonga uy raqamini qo'shish ham o'sha naqsh bilan ishlaydi:

js
const bron = { mijoz: { ism: "Bobur" } };

const yangi = {
  ...bron,
  mijoz: { ...bron.mijoz, manzil: { ...bron.mijoz.manzil, uy: 3 } },
};

console.log(yangi); // { mijoz: { ism: 'Bobur', manzil: { uy: 3 } } }

4.2 Tayinlashda — yo'q

?. faqat o'qish uchun. Uning chap tomonida tayinlash yozib bo'lmaydi:

js
const bron = { mijoz: null };

bron.mijoz?.ism = "Ali";
text
SyntaxError: Invalid left-hand side in assignment

Tarjimasi: "tayinlashning chap tomoni noto'g'ri". SyntaxError — kod hatto ishga tushmaydi (Stack trace va xato turlari). Mantiqi oddiy: bron.mijoz null bo'lsa, ism ni qayerga yozish kerak? JavaScript bu savolga javob bermaydi. Immutable usulda bu savol umuman tug'ilmaydi — biz yozmaymiz, yangi obyekt yasaymiz.

Tekshirib ko'ring: const b = { mijoz: { ism: "Bobur" } }; uchun b.mijoz?.manzil.uy nima beradi? ?. qayerda turishi kerak?

Javob

TypeError: Cannot read properties of undefined (reading 'uy'). ?. faqat o'zidan oldingi qiymatni tekshiradi: b.mijoz bor, shuning uchun .manzil o'qildi — undefined. Keyingi .uy esa oddiy nuqta bilan — xato. To'g'risi: b.mijoz?.manzil?.uy — yo'q bo'lishi mumkin bo'lgan har qavatdan keyin ?..

5. Muzlatish bilan tekshirish

Immutable kod yozdingizmi yoki qayerdadir mutatsiya qolib ketdimi — buni qanday bilasiz? Obyektni himoyalash darsidagi chuqurMuzlat aynan shu ishga yaraydi. Holatni muzlatib, yangilash funksiyasini modulda ishga tushiramiz. Mutatsiya bo'lsa — darhol TypeError:

js
function chuqurMuzlat(obyekt) {
  for (const qiymat of Object.values(obyekt)) {
    if (typeof qiymat === "object" && qiymat !== null) {
      chuqurMuzlat(qiymat);
    }
  }
  return Object.freeze(obyekt);
}

const holat = chuqurMuzlat({
  taomlar: [{ id: 1, nom: "Osh", soni: 1 }],
  mijoz: { ism: "Malika" },
});

function soniniOshir(h, id) {
  return {
    ...h,
    taomlar: h.taomlar.map((t) =>
      t.id === id ? { ...t, soni: t.soni + 1 } : t,
    ),
  };
}

const keyin = soniniOshir(holat, 1);
console.log(keyin.taomlar[0].soni, holat.taomlar[0].soni); // 2 1
console.log(keyin.mijoz === holat.mijoz); // true

Xato chiqmadi — demak soniniOshir muzlagan holatga bir marta ham yozmadi. Ikkinchi qatorga ham qarang: mijoz yo'ldan tashqarida edi va umumiy qoldi — muzlagan obyektni bo'lishish xavfsiz, chunki uni hech kim o'zgartira olmaydi. Agar ichida t.soni++ bo'lganida, modul TypeError: Cannot assign to read only property 'soni' bilan to'xtardi. Redux Toolkit (React ilovalarida holat boshqaradigan kutubxona — keyingi qismlarda) ishlab chiqish rejimida holatni aynan shu sababli muzlatadi.

Tekshirib ko'ring: soniniOshir ichidagi h.taomlar.map(...) ni h.taomlar.forEach((t) => { if (t.id === id) t.soni++; }) ga almashtirsak, bu modulda nima bo'ladi?

Javob

TypeError: Cannot assign to read only property 'soni' of object '#<Object>'. forEach callback'i muzlagan taom obyektiga yozmoqchi bo'ldi. Muzlatish xatoni darhol ushladi — aynan shu uning vazifasi. Muzlatilmagan holatda esa bu kod "ishlardi" va asl holatni jimgina buzardi.

Diqqat: Oddiy <script> da (modul emas) muzlagan obyektga yozish jim e'tiborsiz qoladi (Obyektni himoyalash). U yerda muzlatish ma'lumotni saqlaydi, lekin xatoni ko'rsatmaydi. «Vazifalar qadami» da vazifalar.js aynan shunday skript — shuning uchun u yerda xulqni qo'lda va sinov ro'yxati bilan tekshiramiz.

6. Ko'p uchraydigan xatolar

6.1 Faqat yuqori qavatni nusxalash

const yangi = { ...bron }; yangi.mijoz.ism = "..." — ichki obyekt umumiy, asl ham o'zgaradi. Tuzatish: yo'ldagi har qavatni spread qiling.

6.2 map ichida mutatsiya

arr.map((t) => { t.soni = 3; return t; }) — massiv yangi, obyektlar buzilgan. Tuzatish: (t) => ({ ...t, soni: 3 }).

6.3 sort, reverse, splice holatda

Ular massivning o'zini o'zgartiradi. Tuzatish: toSorted, toReversed, toSpliced, filter.

6.4 ?. tayinlashda

a?.b = 1 — SyntaxError: Invalid left-hand side in assignment. Tuzatish: avval qavat borligini tekshiring yoki immutable usulda yangi obyekt yasang.

6.5 with ga -1 indeks

findIndex topmasa -1 qaytaradi, with(-1, x) esa oxirgi elementni almashtiradi. Tuzatish: map + id taqqoslash yoki -1 ni oldindan tekshirish.

6.6 Meros xususiyatlar nusxaga tushmaydi

Spread faqat obyektning o'z xususiyatlarini ko'chiradi. Obyekt bo'ylab yurish darsidagi Object.create bilan yasalgan buyurtmani eslang — uning valyuta si "meros" edi:

js
const standart = { valyuta: "so'm" };
const buyurtma = Object.create(standart);
buyurtma.taom = "osh";

const nusxa = { ...buyurtma, soni: 2 };

console.log(buyurtma.valyuta, nusxa.valyuta); // so'm undefined

Nusxa oddiy obyekt bo'lib qoldi — merosdan uzildi. Hozircha bu kam uchraydi. Lekin 10-qismning klasslar modulida vazifalar o'z metodlari bilan keladi (vazifa.almashtir() kabi), va metodlar aynan meros orqali ishlaydi (class sintaksisi). Bunday obyektni { ...vazifa } bilan nusxalasangiz, metodlar yo'qoladi. Tuzatish: oddiy ma'lumot obyektlarida spread — to'g'ri yo'l; metodli obyektlarda esa yangilash usulini o'sha obyekt o'zi beradi — buni klasslar darslarida ko'ramiz.

6.7 Spread tartibi adashgan

js
const manzil = { shahar: "Toshkent", uy: 7 };

const yangi = { uy: 12, ...manzil };
console.log(yangi.uy); // 7

Yangi qiymat yozildi — va darhol eski uy uni bosib ketdi. Obyekt literalida keyingi kalit oldingisini almashtiradi (Spread va rest). Tuzatish: avval eskisi, keyin o'zgarish — { ...manzil, uy: 12 }. Qoida: spread — boshida, yangi qiymatlar — oxirida.

7. Mashqlar

1-mashq (oson): Sozlamadagi bitta qiymat

Muzlatilgan sozlamada faqat ovoz.balandlik ni 8 ga o'zgartiring. Asl obyekt va til umumiy qolishi kerak. Ishora: yo'l — sozlama → ovoz → balandlik.

js
const sozlama = Object.freeze({
  til: "uz",
  ovoz: Object.freeze({ yoqilgan: true, balandlik: 5 }),
});
Yechim
js
const sozlama = Object.freeze({
  til: "uz",
  ovoz: Object.freeze({ yoqilgan: true, balandlik: 5 }),
});

const yangi = { ...sozlama, ovoz: { ...sozlama.ovoz, balandlik: 8 } };

console.log(yangi.ovoz.balandlik, sozlama.ovoz.balandlik); // 8 5
console.log(yangi.ovoz.yoqilgan); // true

Ikki spread — yo'ldagi ikki obyekt. Asl muzlagan bo'lgani uchun, agar biz uni o'zgartirmoqchi bo'lsak, modul xato berardi — xato yo'q, demak hammasi to'g'ri.

2-mashq (o'rta): Savatdan o'chirish va qo'shish

Ikki funksiya yozing: taomQosh(savat, taom) va taomOchir(savat, id). Ikkalasi ham yangi savat qaytarsin, eskisiga tegmasin. Savat — { egasi, taomlar: [...] }. Ishora: [...arr, x] va filter.

Yechim
js
function taomQosh(savat, taom) {
  return { ...savat, taomlar: [...savat.taomlar, taom] };
}

function taomOchir(savat, id) {
  return {
    ...savat,
    taomlar: savat.taomlar.filter((t) => t.id !== id),
  };
}

const s0 = Object.freeze({
  egasi: "Aziz",
  taomlar: Object.freeze([]),
});
const s1 = taomQosh(s0, { id: 1, nom: "Osh" });
const s2 = taomQosh(s1, { id: 2, nom: "Manti" });
const s3 = taomOchir(s2, 1);

console.log(s0.taomlar.length, s1.taomlar.length, s2.taomlar.length);
console.log(s3.taomlar.map((t) => t.nom));

Konsolda:

text
0 1 2
[ 'Manti' ]

Har qadam yangi savat berdi, eskilari o'z holicha qoldi — s0 dan s3 gacha butun tarix saqlangan. "Bekor qilish" tugmasi uchun shu tarixning oldingi elementini olish kifoya.

3-mashq (qiyin): Yo'l bo'yicha yangilash funksiyasi

yolBilan(obyekt, yol, qiymat) funksiyasini yozing: yol — kalitlar massivi (["mijoz", "manzil", "uy"]). U yo'l bo'ylab nusxa yasab, oxirgi kalitga qiymat ni qo'ysin. Yo'ldagi qavat yo'q bo'lsa — bo'sh obyekt deb olsin. Ishora: rekursiya — birinchi kalitni oling, qolgan yo'l uchun funksiyani o'zi chaqirsin; massiv destructuring bilan [kalit, ...qolgan].

Yechim
js
function yolBilan(obyekt, yol, qiymat) {
  if (yol.length === 0) {
    return qiymat;
  }
  const [kalit, ...qolgan] = yol;
  const ichki = obyekt?.[kalit];
  return { ...obyekt, [kalit]: yolBilan(ichki, qolgan, qiymat) };
}

const bron = Object.freeze({
  mijoz: Object.freeze({ ism: "Malika" }),
  odam: 4,
});

const yangi = yolBilan(bron, ["mijoz", "manzil", "uy"], 12);

console.log(yangi);
console.log(bron.mijoz);

Konsolda:

text
{ mijoz: { ism: 'Malika', manzil: { uy: 12 } }, odam: 4 }
{ ism: 'Malika' }

To'xtash sharti — yo'l tugadi: endi qiymatning o'zi qaytadi. Har qadamda { ...obyekt, [kalit]: ... } — joriy qavatning nusxasi, bitta kalit almashtirilgan (Computed kalitlar). obyekt?.[kalit] — qavat yo'q bo'lsa (manzil), undefined beradi, keyingi qadamda { ...undefined } esa bo'sh obyekt. Bu funksiya Lodash kutubxonasidagi set ning immutable varianti — faqat massiv indekslarini emas, obyekt kalitlarini qo'llaydi.

4-mashq: Vazifalar qadami — immutable yangilash va modulni yakunlash

refactor/obyektlar branch'idagi oxirgi ish. Obyektni himoyalash darsida ko'rdik: holat.vazifalar ga muzlagan boshlangichVazifalar ni to'g'ridan-to'g'ri bersak, ilova jimgina buziladi — chunki uchta funksiya ma'lumotni hali ham joyida o'zgartiradi:

js
function vazifaQosh(matn) {
  holat.vazifalar.push({
    id: holat.keyingiId,
    matn,
    bajarildi: false,
  });
  // ...
}
function vazifaniAlmashtir(id) {
  const vazifa = vazifaniTop(id);
  vazifa.bajarildi = !vazifa.bajarildi;
  render();
}
function matnniAlmashtir(id, matn) {
  vazifaniTop(id).matn = matn;
  // ...
}

To'rtinchisi — vazifaniOchir — Nusxa bilan o'zgartirish darsidan beri toSpliced bilan yangi massiv beradi. Qolgan uchtasini immutable qiling: har biri holat.vazifalar ga yangi massiv bersin, vazifa obyektlariga tegmasin. So'ng nusxani olib tashlang — vazifalar: boshlangichVazifalar. Xulq o'zgarmasin.

Yechim

Bitta yordamchi funksiya — "id bo'yicha vazifani o'zgartirib, yangi massiv ber":

js
function vazifaniYangila(id, ozgarish) {
  const orin = holat.vazifalar.findIndex((v) => v.id === id);
  holat.vazifalar = holat.vazifalar.with(orin, {
    ...holat.vazifalar[orin],
    ...ozgarish,
  });
}

ozgarish — o'zgaradigan maydonlar obyekti: { bajarildi: true } yoki { matn: "..." }. Ikki spread: avval eski vazifa, keyin o'zgarish — keyingisi bir xil kalitni bosib ketadi. with — yangi massiv, orin dagi element almashtirilgan. Bu funksiya faqat mavjud id bilan chaqiriladi (tugmalar har doim mavjud vazifaga tegishli), shuning uchun -1 holati yo'q.

Uch funksiya endi shunday (vazifaniOchir ni ham qisqartirdik — pastda):

js
function vazifaQosh(matn) {
  holat.vazifalar = [
    ...holat.vazifalar,
    { id: holat.keyingiId, matn, bajarildi: false },
  ];
  holat.keyingiId++;
  if (holat.filtr === "bajarilgan") {
    holat.filtr = "hammasi";
  }
  render();
}

function vazifaniAlmashtir(id) {
  const bajarildi = !vazifaniTop(id).bajarildi;
  vazifaniYangila(id, { bajarildi });
  render();
}

function vazifaniOchir(id) {
  holat.vazifalar = holat.vazifalar.filter((v) => v.id !== id);
  render();
}

function matnniAlmashtir(id, matn) {
  vazifaniYangila(id, { matn });
  holat.tahrirId = null;
  render();
}

Va holat boshida — nusxasiz:

js
const holat = {
  vazifalar: boshlangichVazifalar,
  // keyingiId, filtr, tahrirId va qoldi getter'i — avvalgidek
};

E'tibor bering:

  • vazifaniOchir — filter bilan. toSpliced ham immutable edi, lekin avval findIndex bilan indeks qidirardi. filter((v) => v.id !== id) indekssiz, bitta qatorda o'sha ishni qiladi (filter darsidagi "xavfsiz o'chirish").
  • holat ning o'zi immutable emas. holat.vazifalar = ..., holat.filtr = ... — holat obyektiga yozamiz. Biz faqat vazifalar ma'lumotini (massiv va undagi obyektlarni) o'zgarmas qildik. holat ni ham yangisiga almashtirish (holat = { ...holat, ... }) qoldi getter'ini spread'da "qotirib" qo'yardi (Getter va setter) — bu ilova uchun hozircha kerak emas.
  • Muzlagan ma'lumot — sinov vositasi. Agar biror joyda mutatsiya qolib ketgan bo'lsa, boshlangichVazifalar muzlagani uchun o'sha amal ishlamay qoladi (yoki push xato beradi). Shuning uchun quyidagi ro'yxatning hammasi ishlashi — hamma mutatsiya olib tashlanganining isboti.
  • Bu — oddiy ma'lumot obyektlari uchun. Klasslar modulida vazifa Vazifa klassining nusxasiga aylanadi va o'z metodlari bilan o'zgaradi. O'shanda bu yerdagi spread usuli to'g'ridan-to'g'ri ko'chmaydi — «Meros xususiyatlar nusxaga tushmaydi» bo'limidagi sabab bilan. Bugungi qoida esa qoladi: boshlang'ich ma'lumot buzilmasin.
  • qoldi getter'i o'zgarmadi. U har o'qishda statistika() orqali holat.vazifalar ni yangidan oladi — massiv yangisiga almashganini sezadi.

Eski va yangi funksiyalar bir xil natija berishini Node'da tekshiring — muzlagan boshlang'ich ma'lumot bilan:

js
const boshlangich = Object.freeze([
  Object.freeze({ id: 1, matn: "Non olish", bajarildi: false }),
  Object.freeze({ id: 2, matn: "Takrorlash", bajarildi: true }),
]);

function eskiAlmashtir(royxat, id) {
  const nusxa = royxat.map((v) => ({ ...v }));
  const vazifa = nusxa.find((v) => v.id === id);
  vazifa.bajarildi = !vazifa.bajarildi;
  return nusxa;
}

function yangiAlmashtir(royxat, id) {
  const orin = royxat.findIndex((v) => v.id === id);
  return royxat.with(orin, {
    ...royxat[orin],
    bajarildi: !royxat[orin].bajarildi,
  });
}

const holatga = (r) =>
  r.map((v) => `${v.id}:${v.bajarildi}`).join(" ");
for (const id of [1, 2]) {
  const a = eskiAlmashtir(boshlangich, id);
  const b = yangiAlmashtir(boshlangich, id);
  console.log(holatga(a), "|", holatga(b));
}
console.log(holatga(boshlangich));

Konsolda:

text
1:true 2:true | 1:true 2:true
1:false 2:false | 1:false 2:false
1:false 2:true

Har qatorda ikki ustun bir xil, oxirgi qator esa — muzlagan boshlang'ich ma'lumot o'zgarmagan. Kod modulda ishladi va TypeError bermadi: yangiAlmashtir muzlagan obyektlarga bir marta ham yozmadi.

Brauzerda butun ilovani sinab chiqing — bu besh darsdagi refaktoringning yakuniy tekshiruvi:

  • Ochilganda — uch vazifa, "2 ta vazifa qoldi, 1 ta bajarildi".
  • "Non olish" ni belgilang — chizilib, "1 ta vazifa qoldi, 2 ta bajarildi"; qaytaring — yana "2 ta vazifa qoldi". (Nusxasiz tajribadagidek "katak qaytib ketmaydi".)
  • Tahrirlab Enter — matn haqiqatan almashadi, "O'zgarish saqlandi.", fokus «Tahrirlash» da. Esc — o'zgarishsiz qaytadi.
  • Yangi vazifa qo'shing — konsolda xato yo'q, "Bajarilgan" filtrida qo'shilsa, filtr «Hammasi» ga o'tadi.
  • Hammasini o'chiring — "Hozircha vazifa yo'q. Birinchisini qo'shing!", "0 ta vazifa qoldi, 0 ta bajarildi", fokus "Ro'yxat" sarlavhasida.
  • DevTools konsolida: Object.isFrozen(boshlangichVazifalar[0]) — true, boshlangichVazifalar[0].bajarildi — hamon false: ilova boshlang'ich ma'lumotga tegmadi.

Obyektlar modulida vazifalar.js da nima o'zgardi:

Nima Edi Endi Dars
Qolgan vazifalar soni faqat statistika().qoldi holat.qoldi getter ham Getter va setter
qoldi nusxalarda sanaladi sanalmaydi Deskriptorlar
Boshlang'ich ma'lumot, matnlar oddiy obyekt muzlagan (chuqurMuzlat) Obyektni himoyalash
Filtr nomi tekshirilmaydi Object.hasOwn Object.* metodlari
Vazifalarni o'zgartirish joyida yangi massiv shu dars

Commit va branch'ni yakunlash:

bash
git add assets/js/vazifalar.js
git commit -m "Ro'yxat: vazifalarni immutable yangila"
git switch main
git merge refactor/obyektlar
git branch -d refactor/obyektlar
git push

Pages "Deploy from a branch" rejimida — bir-ikki daqiqadan keyin sayt yangilanadi (Yakuniy loyiha). Telefonda oching: foydalanuvchi hech qanday farq ko'rmasligi kerak.

8. Real ishda

  • React va Redux. setState({ ...holat, mijoz: { ...holat.mijoz, ism } }) va items.map((x) => x.id === id ? { ...x, ... } : x) — React kodida har kuni yoziladigan qatorlar. Redux reducer'lari ham faqat shunday yoziladi. React — 17-qismda.
  • Immer. Uch-to'rt qavatli spread o'qishga qiyin. Immer kutubxonasi "joyida o'zgartirgandek" yozishga ruxsat beradi, ichida esa aynan bugungi yo'l bo'ylab nusxani yasaydi. Uning g'oyasini Immutability chuqur darsida ko'ramiz.
  • "Bekor qilish" va tarix. Matn muharrirlari, rasm dasturlari, forma "qoralamasi" — har holat alohida saqlanadi. Immutable yangilash buni arzon qiladi: o'zgarmagan qismlar umumiy.
  • Intervyu. "Ichma-ich obyektni qanday immutable yangilaysiz?", "nega React'da holatni joyida o'zgartirib bo'lmaydi?", "structuredClone bilan nima farqi bor?" — frontend intervyusining klassik savollari.

Xulosa

  • Immutable yangilash — o'zgartirmaslik, o'zgargan yangi nusxa yasash; eski holat va tarix saqlanadi, o'zgarish === bilan seziladi.
  • Ichma-ich obyektda yo'ldagi har obyekt spread bilan nusxalanadi; yo'ldan tashqaridagilar umumiy qoladi (structural sharing).
  • Massivlarda: qo'shish — [...arr, x], o'chirish — filter, almashtirish — map yoki with, tartiblash — toSorted.
  • ?. chuqur o'qishni xavfsiz qiladi, lekin tayinlashning chap tomonida ishlamaydi.
  • Holatni muzlatib, modulda sinash — qolib ketgan mutatsiyani topishning ishonchli yo'li.

Keyingi dars: JSON asoslari — obyektni matnga aylantirib saqlash va boshqa dasturga yuborishni o'rganamiz.

Manbalar

  • MDN: "Spread syntax", "Optional chaining", "Array.prototype.with()" — developer.mozilla.org
  • React hujjatlari: "Updating Objects in State", "Updating Arrays in State" — react.dev
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript'da immutable yangilash: ichma-ich obyekt va massivni asl nusxaga tegmasdan o'zgartirish — IlmHamroh