IlmHamroh
JavaScript Full-stack/10-qism. JavaScript chuqur65/65-dars51 daqiqa
Mundarija (42)

JavaScript yakuniy loyiha: vazifalar v2 — modullar, klasslar va closure

Qisqacha: Bu darsda 10-qism davomida qadamma-qadam o'zgargan vazifalar ilovasini bitta toza arxitekturaga yig'amiz. Ilova oltita ES modulga bo'linadi: Vazifa va VazifalarRoyxati klasslari, closure bilan id, HOF bilan ro'yxat va hisob, Map/Set bilan teglar, JSON eksport va xatolarni ushlaydigan import. Foydalanuvchi uchun xulq o'zgarmaydi — buni Node 24 da va brauzerda oldindan yozilgan ssenariy bilan tekshiramiz.

Bu darsda

  • 10-qism bilimlarini — HOF, obyektlar, closure, klass, Map/Set, iteratsiya, xatolar va modullar — bitta ishlaydigan ilovaga yig'asiz.
  • Refaktorni reja bilan qilasiz: avval tekshiruv ssenariysi, keyin pastdan yuqoriga, modulma-modul.
  • Qism davomida qadamma-qadam qo'shilgan kodni ko'zdan kechirib, ortiqchasini tozalaysiz: importdagi xavfsizBirlashtir va vazifaniOl dagi standart qiymatlar.
  • Mantiq qatlamini brauzersiz — Node 24 da — tekshirasiz.
  • vazifalar v2 ni PR orqali main ga qo'shib, GitHub Pages'ga chiqarasiz va 100 ballik jadval bilan baholaysiz.

Oldin bilishingiz kerak: Yakuniy loyiha: vazifalar v1, OOP tamoyillari va kompozitsiya, Xato strategiyasi, ESM import, CommonJS va ESM.

1. Nega bu kerak?

09-qism oxirida vazifalar bitta vazifalar.js faylidan iborat edi. 10-qismda unga deyarli har darsda bitta qadam qo'shildi: filter va reduce, getter, closure, klasslar, teglar, eksport, import va nihoyat modullar. Har qadam o'z branch'ida, o'z mavzusi bilan qilindi.

Har modul (dars guruhi) bitta qadam qo'shdi, Nega modullar — CommonJS va ESM darslarida esa kod oltita modulga bo'lindi. Funksiyalarning ichi esa ataylab o'zgarmay ko'chdi: refaktorda bir vaqtda ham joyini, ham ichini o'zgartirmaymiz. Endi ichini ko'zdan kechirish navbati:

  • import: paket.js da Prototype pollution darsidagi xavfsizBirlashtir hamon turibdi, vazifaniOl esa yetishmagan maydonga standart qiymat qo'yadi — Xato strategiyasi dagi tekshiruvdan keyin ikkalasi ham ortiqcha;
  • tekshiruv: tekshiruv/royxat.js faqat royxatni sinaydi — import, teglar va muzlagan ma'lumot tekshirilmagan; README esa v2 ni tasvirlamaydi.

Bu ta'mirdan keyingi uyga o'xshaydi. Har usta o'z ishini yaxshi qildi, lekin asboblar har xonada sochilib qoldi. Endi hamma narsani o'z joyiga qo'yamiz.

Arxitektura (architecture) — dasturning qismlarga bo'linishi va qismlar bir-biri bilan qanday bog'lanishi. Xuddi uy rejasi kabi: qaysi xona nima uchun, qaysi eshik qayerga ochiladi. Bugun vazifalar ning maqsadli arxitekturasini quramiz.

Yangi mavzu yo'q. 64 dars bilimi bitta loyihada uchrashadi. Bu real ishdagi eng ko'p uchraydigan vazifalardan biri: ishlab turgan kodni buzmasdan qayta tuzish.

2. Loyiha talablari

2.1 Ilova nima qiladi

Foydalanuvchi uchun v2 — bu v1 va 10-qismda qo'shilgan to'rt imkoniyat:

  1. Asosiy amallar — 09-qismdagidek: qo'shish (bo'sh va takror matn rad etiladi), belgilash, tahrirlash (Enter/Esc), o'chirish, uch filtr. Fokus hech qachon yo'qolmaydi (Yakuniy loyiha: v1).
  2. Saralash. Bajarilmaganlar tepada, guruh ichida — qo'shilgan tartib (sort).
  3. Statistika. "2 ta vazifa qoldi, 1 ta bajarildi" (Guruhlash).
  4. Teglar. "Teglar: #bozor — 2, #uy — 1. Yopilgan: #uy" (Set to'plam metodlari).
  5. Eksport va import. { versiya: 1, vazifalar } JSON matni. Buzuq matn aniq xabar bilan rad etiladi, ro'yxat esa o'zgarmaydi (Xato strategiyasi).

Saqlash hali yo'q: sahifa yangilansa, ro'yxat boshlang'ich holatiga qaytadi. Bu — 11-qismning ishi.

2.2 Refaktorning oltin qoidasi

Bu loyihada yangi imkoniyat qo'shilmaydi — biz kodni qayta tuzamiz. Shuning uchun bitta qoida hamma narsadan ustun: xulq o'zgarmaydi. Foydalanuvchi bir xil tugmani bossa, bir xil natijani, bir xil matnni va bir xil fokusni ko'rishi kerak.

Xulq buzilganini qanday bilamiz? Ko'z bilan chamalab emas — oldindan yozilgan ro'yxat bilan. Regressiya (regression) — avval ishlagan narsaning yangi o'zgarishdan keyin buzilishi. Xuddi ta'mirdan keyin chiroq yonmay qolishi kabi: usta boshqa simga tekkan. Regressiyani ushlash uchun har bosqichdan keyin bir xil tekshiruvni takrorlaymiz.

Yana uch qoida:

  • Pastdan yuqoriga. Avval hech kimga bog'liq bo'lmagan vazifa.js, oxirida hammani ulaydigan asosiy.js. Har yangi fayl tayyor poydevorga tayanadi.
  • Faqat sababi bor joyni o'zgartiring. Funksiya tanasi faqat ortiqcha yoki boshqacha yozilgan kodni tozalash uchun qayta yoziladi.
  • Strelkalar faqat pastga. Modul o'zidan yuqoridagini import qilmaydi: aylana import TDZ xatosiga olib keladi (Modul semantikasi).

Tekshirib ko'ring: Refaktor paytida "yo'l-yo'lakay" yangi imkoniyat ham qo'shib ketish nega xavfli?

Javob

Xulq o'zgarsa, sababini ajratib bo'lmaydi: bu refaktordagi xatomi yoki yangi imkoniyatning natijasimi? Tekshiruv ro'yxati ham "nima kutilgan?" degan savolga javob berolmay qoladi. Shuning uchun bitta branch — bitta maqsad: avval qayta tuzish, yangi imkoniyat — keyin, alohida.

2.3 Maqsadli arxitektura

Loyiha oxirida fayllar shunday turadi:

text
vazifalar/
├── index.html
├── package.json
├── README.md
├── assets/
│   ├── css/
│   │   └── vazifalar.css
│   └── js/
│       ├── asosiy.js
│       ├── holat.js
│       ├── paket.js
│       ├── render.js
│       ├── royxat.js
│       └── vazifa.js
└── tekshiruv/
    ├── paket.js
    ├── royxat.js
    └── ssenariy.md

Har modul — bitta savolga javob:

Fayl Nima uchun javob beradi Eksport qiladi
vazifa.js bitta vazifa va uning xatosi Vazifa, VazifaXatosi
royxat.js to'plam: so'rov, o'zgartirish, obuna VazifalarRoyxati
paket.js JSON eksport va import eksportMatni, paketniOqi
holat.js ilova holati va o'zgartiruvchilar holat, boshlangichVazifalar, 7 funksiya
render.js DOM: chizish, fokus, e'lon render, elementlar, yordamchilar
asosiy.js tinglovchilar va ishga tushirish hech narsa

paket.js nomi haqida: eksport va importning umumiy mavzusi — paket: { versiya, vazifalar } obyekti (paketniOqi nomidagi so'z). U DOM'ga ham, holatga ham tegmaydi — shuning uchun alohida modul.

Kim kimni import qiladi:

flowchart TD
  A["asosiy.js<br/>tinglovchilar"] --> R["render.js<br/>DOM"]
  A --> H["holat.js<br/>holat"]
  A --> P["paket.js<br/>eksport, import"]
  A --> RO["royxat.js<br/>VazifalarRoyxati"]
  A --> V["vazifa.js<br/>Vazifa, VazifaXatosi"]
  R --> H
  R --> V
  H --> RO
  P --> V
  RO --> V

Diagrammaga qarang: strelka "import qiladi" degani. vazifa.js hech kimga bog'liq emas, asosiy.js ni esa hech kim import qilmaydi. Aylana yo'q. Pastki to'rtta modul — vazifa, royxat, paket, holat — DOM'ga tegmaydi, demak Node'da ham ishlaydi.

3. Reja: sakkiz bosqich

# Bosqich Natija Tayanch dars
1 Branch va ssenariy tekshiruv/ssenariy.md Debugging
2 vazifa.js Vazifa, VazifaXatosi Private maydon
3 royxat.js klass, closure, HOF, teglar Kompozitsiya
4 holat.js muzlagan boshlang'ich ma'lumot Obyektni himoyalash
5 paket.js eksport va xavfsiz import Xato strategiyasi
6 render.js, asosiy.js ilova brauzerda ESM import
7 Node tekshiruvi tekshiruv/ — hammasi CommonJS va ESM
8 PR va Pages v2 internetda GitHub CLI

Har bosqichda avval faylingizni o'zingiz ko'zdan kechiring: u qaysi darslardan yig'ilgan va nimasi ortiqcha? Keyin yechim bilan solishtiring va farq bo'lsa — yechimdagini oling. 2–5-bosqichlarda brauzer emas, Node tekshiradi — bu fayllar DOM'ga tegmaydi. Fayl nomlari va eksportlar o'zgarmaydi, shuning uchun sahifa har bosqichdan keyin ishlab turadi. main ga esa faqat to'liq tekshirilgan holat tushadi.

4. 1-bosqich: branch va oltin ssenariy

bash
cd ~/kurs/vazifalar
git switch main
git pull
git switch -c feature/v2

Branch nomi — versiya: undan main ga vazifalar ning ikkinchi versiyasi tushadi.

Endi kod yozishdan oldin — tekshiruv. Quyidagi 14 qadam — oltin ssenariy: refaktordan keyin ilova aynan shunday ishlashi kerak. Uni tekshiruv/ssenariy.md fayliga ko'chiring — bu Birinchi avtomatik test darsigacha bizning qo'lda tekshiruv ro'yxatimiz.

# Harakat Kutilgan natija
1 Sahifani ochish "Non olish", "Onamga qo'ng'iroq qilish", chizilgan "DOM darsini takrorlash"; "2 ta vazifa qoldi, 1 ta bajarildi"; Console toza
2 Bo'sh maydonda Enter "Vazifa matnini yozing.", fokus maydonda
3 "non OLISH" qo'shish "Bu vazifa ro'yxatda allaqachon bor."
4 "Sabzi olish #bozor #uy" faollar oxirida; "Teglar: #bozor — 1, #uy — 1"
5 "Non olish" katagi pastga tushadi, fokus o'z katagida
6 «Tahrirlash» → matn → Enter "O'zgarish saqlandi.", fokus «Tahrirlash» da
7 «Tahrirlash» → Esc matn eskicha, fokus «Tahrirlash» da
8 «Faol» da belgilash vazifa chiqadi, fokus qo'shni katakda
9 «Bajarilgan» da qo'shish filtr «Hammasi» ga o'tadi
10 «O'chirish» "«…» o'chirildi.", fokus qo'shni «O'chirish» da
11 «JSON qilib chiqarish» "versiya": 1 li matn, to'liq belgilangan
12 O'z eksportini import "N ta vazifa import qilindi."
13 {"versiya": 2, ...} import "Versiya 2 o'qilmaydi — faqat 1.", ro'yxat o'zgarmaydi
14 Hammasini o'chirish "Hozircha vazifa yo'q…", fokus "Ro'yxat" sarlavhasida

Bu — 10-qism oxiridagi kelishilgan xulq. Sizning main ingizda biror imkoniyat boshqacha bo'lsa (masalan, biror darsning qadamini o'tkazib yuborgan bo'lsangiz), jadvaldagi natija — maqsad.

bash
git add tekshiruv/ssenariy.md
git commit -m "Docs: oltin ssenariyni yoz"

5. 2-bosqich: vazifa.js

Eng pastki modul. Unda ikki klass: Private maydon va static a'zolar darslaridagi Vazifa hamda O'z xato klasslaringiz darsidagi VazifaXatosi. Faylingiz shunday bo'lishi kerak:

js
// vazifa.js — bitta vazifa va vazifalar xatosi

export class VazifaXatosi extends Error {
  constructor(xabar, sozlama) {
    super(xabar, sozlama);
    this.name = "VazifaXatosi";
    this.kod = sozlama?.kod ?? "NOMALUM";
  }
}

export class Vazifa {
  // index.html dagi maxlength bilan bir xil bo'lsin
  static MAKS_UZUNLIK = 100;

  id;
  bajarildi;
  #matn;

  constructor(id, matn, bajarildi = false) {
    this.id = id;
    this.#matn = matn;
    this.bajarildi = bajarildi;
  }

  static dan(malumot) {
    return new Vazifa(malumot.id, malumot.matn, malumot.bajarildi);
  }

  get matn() {
    return this.#matn;
  }

  matnniOzgartir(yangi) {
    const toza = yangi.trim();
    if (toza === "" || toza.length > Vazifa.MAKS_UZUNLIK) {
      return false;
    }
    this.#matn = toza;
    return true;
  }

  almashtir() {
    this.bajarildi = !this.bajarildi;
  }

  toJSON() {
    return {
      id: this.id,
      matn: this.#matn,
      bajarildi: this.bajarildi,
    };
  }
}

Nimani tekshiring:

  • VazifaXatosi — kod bilan. Import kod ga tayanadi ("JSON", "VERSIYA", "VAZIFA"). Faylingizda kod yo'q bo'lsa — O'z xato klasslaringiz darsidagidek qo'shing.
  • Vazifa — o'zgarishsiz. #matn private, o'qish getter orqali, o'zgartirish matnniOzgartir orqali. U xato tashlamaydi, true/false qaytaradi: tahrirda noto'g'ri matn — oddiy holat, istisno emas.
  • toJSON va static dan — juftlik. Biri klassdan oddiy obyektga, ikkinchisi oddiy obyektdan klassga. Eksport, import va 11-qismdagi saqlash shu ikkisiga tayanadi.

Tekshiruv:

  • node --check assets/js/vazifa.js — hech narsa chiqmaydi, sintaksis to'g'ri.
  • Faylda bitta ham import yo'q — grafikning eng pastki qatori.

6. 3-bosqich: royxat.js

Ilovaning yuragi — 10-qismning eng ko'p bilimi shu faylda uchrashadi. Statistika, saralash, teglar, iterator va kuzatuvchi unga darsma-dars qo'shilgan. Faylingizni yechim bilan solishtiring — farq bo'lmasligi kerak. Bu bosqich — fayldagi har qarorni qaysi darsdan kelganini bilib, qayta o'qish.

js
// royxat.js — vazifalar to'plami: so'rovlar, o'zgartirishlar, obuna
import { Vazifa } from "./vazifa.js";

// Quyidagi beshtasi eksport qilinmaydi — faqat shu modul ichida
function idYasovchiYarat(boshlangich) {
  let keyingi = boshlangich;
  return () => keyingi++;
}

function faollarOldin(a, b) {
  return Number(a.bajarildi) - Number(b.bajarildi);
}

function guruhNomi(vazifa) {
  return vazifa.bajarildi ? "bajarilgan" : "faol";
}

function teglariniOl(matn) {
  return new Set(
    matn
      .toLowerCase()
      .split(" ")
      .filter((soz) => soz.length > 1 && soz.startsWith("#")),
  );
}

function tegToplami(vazifalar) {
  return new Set(
    vazifalar.flatMap((vazifa) => [...teglariniOl(vazifa.matn)]),
  );
}

export class VazifalarRoyxati {
  #vazifalar = [];
  #yangiId = idYasovchiYarat(1);
  #tinglovchilar = [];

  static dan(malumotlar) {
    const yangi = new VazifalarRoyxati();
    yangi.#vazifalar = malumotlar.map((m) => Vazifa.dan(m));
    const engKatta = malumotlar.reduce(
      (eng, m) => Math.max(eng, m.id),
      0,
    );
    yangi.#yangiId = idYasovchiYarat(engKatta + 1);
    return yangi;
  }

  static mosmi(vazifa, filtr) {
    if (filtr === "faol") {
      return !vazifa.bajarildi;
    }
    if (filtr === "bajarilgan") {
      return vazifa.bajarildi;
    }
    return true;
  }

  // So'rovlar: royxatni o'zgartirmaydi
  statistika() {
    const guruhlar = Object.groupBy(this.#vazifalar, guruhNomi);
    return {
      qoldi: guruhlar.faol?.length ?? 0,
      bajarildi: guruhlar.bajarilgan?.length ?? 0,
    };
  }

  get qoldi() {
    return this.statistika().qoldi;
  }

  korinadiganlar(filtr) {
    return this.#vazifalar
      .filter((v) => VazifalarRoyxati.mosmi(v, filtr))
      .toSorted(faollarOldin);
  }

  top(id) {
    return this.#vazifalar.find((v) => v.id === id);
  }

  matnBormi(matn, ozId) {
    const kichik = matn.toLowerCase();
    return this.#vazifalar.some(
      (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
    );
  }

  tegHisobi() {
    return this.#vazifalar
      .flatMap((vazifa) => [...teglariniOl(vazifa.matn)])
      .reduce(
        (sanoq, teg) => sanoq.set(teg, (sanoq.get(teg) ?? 0) + 1),
        new Map(),
      );
  }

  yopilganTeglar() {
    const bajarilgan = this.#vazifalar.filter((v) => v.bajarildi);
    const faol = this.#vazifalar.filter((v) => !v.bajarildi);
    return tegToplami(bajarilgan).difference(tegToplami(faol));
  }

  // Kuzatuvchi: royxat o'zgarsa, obunachilarga xabar
  obuna(tinglovchi) {
    this.#tinglovchilar.push(tinglovchi);
    return () => {
      this.#tinglovchilar = this.#tinglovchilar.filter(
        (t) => t !== tinglovchi,
      );
    };
  }

  #xabarBer() {
    this.#tinglovchilar.forEach((tinglovchi) => tinglovchi());
  }

  // O'zgartiruvchilar: har biri oxirida #xabarBer()
  qosh(matn) {
    const vazifa = new Vazifa(this.#yangiId(), matn);
    this.#vazifalar.push(vazifa);
    this.#xabarBer();
    return vazifa;
  }

  almashtir(id) {
    this.top(id).almashtir();
    this.#xabarBer();
  }

  matnniOzgartir(id, matn) {
    const ozgardi = this.top(id).matnniOzgartir(matn);
    this.#xabarBer();
    return ozgardi;
  }

  ochir(id) {
    this.#vazifalar = this.#vazifalar.filter((v) => v.id !== id);
    this.#xabarBer();
  }

  *[Symbol.iterator]() {
    yield* this.#vazifalar;
  }

  toJSON() {
    return this.#vazifalar.map((v) => v.toJSON());
  }
}

6.1 Seyf: private maydonlar, closure va kuzatuvchi

  • #yangiId = idYasovchiYarat(...). Ikki g'oya birga: Closure amaliyotda darsidagi hisoblagich OOP tamoyillari darsida private maydonga joylashdi. Closure private maydonda turadi: unga na tashqi kod, na klassning boshqa metodlari tegadi — faqat this.#yangiId() chaqiriladi.
  • engKatta + 1. Closure amaliyotda darsida boshlang'ich 4 qo'lda yozilgan edi, "11-qismda yuklangan ro'yxatdan hisoblanadi" degan va'da bilan. Va'da bajarildi: dan istalgan ro'yxat uchun keyingi id ni o'zi hisoblaydi — import ham shu yo'ldan o'tadi.
  • Eksportsiz funksiyalar. idYasovchiYarat, faollarOldin, guruhNomi, teglariniOl, tegToplami — modulning ichki yordamchilari. Tashqariga faqat klass chiqadi: modulning o'zi ham "seyf" (Nega modullar).
  • obuna va #xabarBer (Dizayn naqshlari). Har o'zgartiruvchi metod oxirida obunachilarga xabar beradi. render() ni chaqirishni hech kim eslab qolishi shart emas.
  • *[Symbol.iterator] (Generatorlar chuqur). [...royxat] va for...of ishlaydi, ichki massiv esa tashqariga chiqmaydi.

6.2 So'rovlar: HOF, guruhlash, Map va Set

Ish Qaysi darsdan Qanday
Hisob Guruhlash, Getter va setter statistika() — Object.groupBy; get qoldi unga tayanadi
Ko'rinadiganlar Nusxa bilan o'zgartirish, OOP korinadiganlar(filtr) metodi, toSorted bilan
Teglar Map, Set tegHisobi, yopilganTeglar — royxat metodlari
O'chirish OOP filter — bitta qator
  • statistika() — yagona hisob manbai. Getter va setter darsidagi qoldi o'z hisobini yuritmaydi — statistika().qoldi ni qaytaradi. Ikki joyda sanash — ikki xil javob xavfi. Object.groupBy bo'sh guruhni yaratmaydi, shuning uchun guruhlar.faol?.length ?? dan keyin turadi.
  • toSorted — #vazifalar tartibi hech qachon o'zgarmaydi, ekran uchun saralangan nusxa olinadi (Nusxa bilan o'zgartirish).
  • Teglar royxat orqali. tegHisobi (Map) va yopilganTeglar (Set to'plam metodlari) vazifalar to'plami haqidagi savol — joyi ham to'plamda. Ular global holat ni emas, this.#vazifalar ni o'qiydi.
  • ochir — filter bilan. findIndex + splice ikki qadam va bitta tuzoq bo'lardi: id topilmasa findIndex -1 beradi, splice(-1, 1) esa oxirgi vazifani o'chiradi. filter bitta qatorda yangi massiv yasaydi — royxatning qolgan metodlari kabi.
  • push klass ichida — xavfsiz. Immutable yangilash qoidasi oddiy ma'lumot uchun edi. #vazifalar ni esa tashqi kod umuman ko'rmaydi. Tashqariga doim yangi massiv chiqadi: korinadiganlar ham, toJSON ham.

Tekshirib ko'ring: holat.vazifalar.korinadiganlar("faol").push(x) ro'yxatga vazifa qo'shadimi?

Javob

Yo'q. filter va toSorted — ikkalasi ham yangi massiv qaytaradi. push o'sha nusxani o'zgartiradi, #vazifalar esa tegilmay qoladi. Ro'yxatga qo'shishning yagona yo'li — qosh(): u id beradi va obunachilarga xabar qiladi.

Tekshiruv: node --check assets/js/royxat.js. To'liq sinov — 7-bosqichda.

7. 4-bosqich: holat.js

Bu faylda bitta so'z o'zgaradi — boshlangichVazifalar oldiga export qo'shiladi:

js
// oldin
const boshlangichVazifalar = chuqurMuzlat([
// keyin
export const boshlangichVazifalar = chuqurMuzlat([

Nega? 7-bosqichda Node'da Object.isFrozen(boshlangichVazifalar[0]) ni so'raymiz — javob bo'lishi kerak. Tekshiruv fayli bu qiymatni o'qishi uchun u eksport qilinishi shart. Qolgan hamma narsa ESM import darsidagidek.

To'liq fayl: assets/js/holat.js
js
// holat.js — ilova holati: yagona haqiqat manbai
import { VazifalarRoyxati } from "./royxat.js";

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

export const boshlangichVazifalar = chuqurMuzlat([
  { id: 1, matn: "Non olish", bajarildi: false },
  { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
  { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
]);

export const holat = {
  vazifalar: VazifalarRoyxati.dan(boshlangichVazifalar),
  filtr: "hammasi",
  tahrirId: null,
};

// So'rovlar: faqat qiymat qaytaradi
export function xatoniTop(matn, vazifalar, ozId) {
  if (matn === "") {
    return "Vazifa matnini yozing.";
  }
  if (vazifalar.matnBormi(matn, ozId)) {
    return "Bu vazifa ro'yxatda allaqachon bor.";
  }
  return null;
}

export function korinadiganlar() {
  return holat.vazifalar.korinadiganlar(holat.filtr);
}

export function vazifaniTop(id) {
  return holat.vazifalar.top(id);
}

// O'zgartiruvchilar: avval holat, oxirida royxat (u render'ni
// kuzatuvchi orqali chaqiradi)
export function vazifaQosh(matn) {
  if (holat.filtr === "bajarilgan") {
    holat.filtr = "hammasi";
  }
  holat.vazifalar.qosh(matn);
}

export function vazifaniAlmashtir(id) {
  holat.vazifalar.almashtir(id);
}

export function vazifaniOchir(id) {
  holat.vazifalar.ochir(id);
}

export function matnniAlmashtir(id, matn) {
  holat.tahrirId = null;
  holat.vazifalar.matnniOzgartir(id, matn);
}

Fayldagi to'rt qarorni eslang:

  • chuqurMuzlat (Obyektni himoyalash). Boshlang'ich ma'lumot — oddiy obyektlar, chuqur muzlagan. VazifalarRoyxati.dan ulardan yangi Vazifa nusxalarini yasaydi. Shuning uchun muzlagan ma'lumotga hech kim yozmaydi va unga nusxa ham kerak emas.
  • Klass nusxasi spread bilan nusxalanmaydi. { ...vazifa } da na #matn, na metodlar bor (Immutable yangilash darsidagi ogohlantirish). Shuning uchun Vazifa ni o'zgartirish — faqat uning metodlari orqali.
  • holat — uch maydon (Kompozitsiya): royxat, filtr va tahrir. Id hisoblagichi holatda emas — royxat ichida (#yangiId).
  • Tartib muhim (Dizayn naqshlari). vazifaQosh da avval filtr, matnniAlmashtir da avval tahrirId, oxirida royxat. Royxat o'zgarishi bilan render ishlaydi — unga kerak bo'lgan holat tayyor bo'lishi shart.
bash
git add assets/js/holat.js
git commit -m \
  "Vazifalar: boshlang'ich ma'lumotni tekshiruvga eksport qil"

8. 5-bosqich: paket.js

Bu bosqichda kod haqiqatan o'zgaradi. Prototype pollution darsidan beri paket.js ga himoya qatlamlari birin-ketin qo'shildi. Xato strategiyasi dagi malumotlarniTekshir kelgach, ulardan ikkitasi ortiqcha bo'lib qoldi. Uch joyni o'zgartiramiz.

8.1 Maydonlar ro'yxati — bitta joyda

js
// oldin
const XAVFLI_KALITLAR = ["__proto__", "constructor", "prototype"];
const VAZIFA_MAYDONLARI = ["id", "matn", "bajarildi"];
const eksportMaydonlari = [
  "versiya", "vazifalar", "id", "matn", "bajarildi",
];
js
// keyin
const VAZIFA_MAYDONLARI = ["id", "matn", "bajarildi"];
const eksportMaydonlari = [
  "versiya", "vazifalar", ...VAZIFA_MAYDONLARI,
];

Avval vazifa maydonlari ikki joyda yozilgan edi. Ertaga to'rtinchi maydon (masalan, muddat) qo'shilsa, birini unutish oson. Endi eksportMaydonlari VAZIFA_MAYDONLARI dan spread bilan yasaladi. XAVFLI_KALITLAR esa butunlay ketadi — sababi «xavfsizBirlashtir olib tashlanadi» bo'limida.

8.2 vazifaniOl — standart qiymatsiz

js
// oldin
function vazifaniOl(xom) {
  const vazifa = { id: 0, matn: "", bajarildi: false };
  for (const maydon of VAZIFA_MAYDONLARI) {
js
// keyin
function vazifaniOl(xom) {
  const vazifa = {};
  if (!oddiyObyektmi(xom)) {
    return vazifa;
  }
  for (const maydon of VAZIFA_MAYDONLARI) {

Prototype pollution darsida vazifaniOl yetishmagan maydonga standart qiymat qo'yardi — o'shanda tekshiruv hali yo'q edi. Endi malumotlarniTekshir id: 0 ni baribir rad etadi, standart qiymat faqat chalg'itadi. Uning o'rniga boshida oddiyObyektmi tekshiruvi turadi.

Bu — xulqdagi ikki ongli o'zgarish, oltin qoidadan istisno:

  • bajarildi siz vazifa endi "bajarildi true yoki false emas." bilan rad etiladi. Avval u jimgina false olardi.
  • "vazifalar": [null] endi "1-vazifa: id …" xabarini oladi. Avval Object.hasOwn(null, …) TypeError berardi va foydalanuvchi "Kutilmagan xato" ni ko'rardi.

Bizning eksport uchala maydonni doim yozadi, shuning uchun o'z eksportingizni import qilish avvalgidek ishlaydi.

8.3 xavfsizBirlashtir olib tashlanadi

js
// oldin — paketniOqi ichida
  const paket = xavfsizBirlashtir({}, xom);
  if (paket.versiya !== 1) {
  // ...
  const malumotlar = paket.vazifalar.map(vazifaniOl);
js
// keyin
  if (xom.versiya !== 1) {
  // ...
  const malumotlar = xom.vazifalar.map(vazifaniOl);

xavfsizBirlashtir funksiyasi va XAVFLI_KALITLAR fayldan butunlay o'chiriladi. Nega bu xavfsiz? Xavf birlashtirishda edi: __proto__ kaliti nishon obyektga yozilganda uning prototipini o'zgartiradi. JSON.parse esa __proto__ ni oddiy, o'z kaliti qilib yaratadi — u hech narsani buzmaydi. Biz paketdan faqat versiya va vazifalar ni o'qiymiz, vazifadan esa oq ro'yxatdagi uch kalitni Object.hasOwn bilan olamiz. Birlashtirish yo'q — demak uning himoyasi ham ortiqcha. Bu tamoyilning nomi — YAGNI ("bu sizga kerak bo'lmaydi"). __proto__ li paket bilan sinov 7-bosqichda.

Tekshirib ko'ring: vazifaniOl dagi standart qiymatlar nega yordam emas, balki chalkashlik edi?

Javob

Standart qiymat buzuq ma'lumotni jimgina "tuzatib" yuborardi: id siz vazifa id: 0 olardi va muammo keyinroq, boshqa joyda chiqardi. Tekshiruv esa uni chegarada, aniq xabar bilan to'xtatadi (fail fast). Bir vaqtda ham "tuzatish", ham "rad etish" — bir-biriga zid ikki qoida.

8.4 Tayyor fayl

Eksport va import qanday ishlashi o'zgarmadi:

Eksport (JSON asoslari, JSON chuqur). eksportMatni(holat.vazifalar) royxatni oladi. JSON.stringify uning toJSON ini chaqiradi va oddiy massiv oladi, keyin oq ro'yxat ortiqcha kalitni tashlaydi.

Import (try / catch — Xato strategiyasi). Ichki try faqat JSON.parse ni o'raydi va asl SyntaxError ni cause ga qo'yadi. Har buzilgan qoida — o'z kod li VazifaXatosi. malumotlarniTekshir birinchi xatodayoq to'xtaydi (fail fast). Holatga bu faylda umuman tegilmaydi — "hammasi yoki hech narsa" tinglovchida ta'minlanadi.

To'liq fayl: assets/js/paket.js
js
// paket.js — eksport va import: {versiya: 1, vazifalar: [...]}
import { Vazifa, VazifaXatosi } from "./vazifa.js";

const VAZIFA_MAYDONLARI = ["id", "matn", "bajarildi"];
const eksportMaydonlari = [
  "versiya", "vazifalar", ...VAZIFA_MAYDONLARI,
];

export function eksportMatni(vazifalar) {
  const paket = { versiya: 1, vazifalar };
  return JSON.stringify(paket, eksportMaydonlari, 2);
}

function oddiyObyektmi(qiymat) {
  return typeof qiymat === "object" && qiymat !== null
    && !Array.isArray(qiymat);
}

// Oq ro'yxat: faqat uch maydon va faqat o'z kalitlari
function vazifaniOl(xom) {
  const vazifa = {};
  if (!oddiyObyektmi(xom)) {
    return vazifa;
  }
  for (const maydon of VAZIFA_MAYDONLARI) {
    if (Object.hasOwn(xom, maydon)) {
      vazifa[maydon] = xom[maydon];
    }
  }
  return vazifa;
}

function vazifaXatosi(tartib, sabab) {
  return new VazifaXatosi(`${tartib}-vazifa: ${sabab}`, {
    kod: "VAZIFA",
  });
}

function malumotlarniTekshir(malumotlar) {
  const idlar = new Set();
  malumotlar.forEach((v, i) => {
    const tartib = i + 1;
    if (!Number.isInteger(v.id) || v.id < 1 || idlar.has(v.id)) {
      throw vazifaXatosi(tartib, "id musbat, takrorlanmas son emas.");
    }
    idlar.add(v.id);
    if (typeof v.matn !== "string" || v.matn.trim() === "") {
      throw vazifaXatosi(tartib, "matn bo'sh.");
    }
    if (v.matn.length > Vazifa.MAKS_UZUNLIK) {
      throw vazifaXatosi(tartib, "matn juda uzun.");
    }
    if (typeof v.bajarildi !== "boolean") {
      throw vazifaXatosi(tartib, "bajarildi true yoki false emas.");
    }
  });
}

export function paketniOqi(matn) {
  let xom;
  try {
    xom = JSON.parse(matn);
  } catch (xato) {
    throw new VazifaXatosi(
      "Matn JSON emas. Eksportni to'liq oling.",
      { kod: "JSON", cause: xato },
    );
  }
  if (!oddiyObyektmi(xom)) {
    throw new VazifaXatosi("Bu matn vazifalar eksporti emas.", {
      kod: "TUZILMA",
    });
  }
  if (xom.versiya !== 1) {
    throw new VazifaXatosi(
      `Versiya ${xom.versiya} o'qilmaydi — faqat 1.`,
      { kod: "VERSIYA" },
    );
  }
  if (!Array.isArray(xom.vazifalar)) {
    throw new VazifaXatosi("Matnda vazifalar ro'yxati yo'q.", {
      kod: "TUZILMA",
    });
  }
  const malumotlar = xom.vazifalar.map(vazifaniOl);
  malumotlarniTekshir(malumotlar);
  return malumotlar;
}

Tekshiruv: node --check assets/js/paket.js. Fayl document ni bilmaydi — u ham Node'da sinaladi.

bash
git add assets/js/paket.js
git commit -m "Vazifalar: importdagi ortiqcha himoyani olib tashla"

9. 6-bosqich: render.js, asosiy.js va index.html

render.js, asosiy.js va index.html ESM import va Brauzerda ESM darslaridagidek qoladi — yechim bilan solishtiring, farq bo'lmasligi kerak. Bu bosqich — ularni qayta o'qib, butun ilova qanday ulanganini ko'rish.

9.1 render.js — ekran qatlami

Ekranni faqat shu fayl chizadi. Uni o'qiganda uch narsaga qarang:

  • Hisob — statistika() dan, teglar qatori — teglarniYoz bilan. Ikkalasi ham royxatdan so'raydi, o'zi sanamaydi.
  • boshMatnlar eksport qilinadi. U uchta to'g'ri filtr nomining yagona ro'yxati — filtrniTanla shundan foydalanadi.
  • DOM bilan ishlaydigan hamma narsa shu yerda: eksport va import elementlari ham, importXatosiniKorsat ham. asosiy.js document.getElementById ni deyarli chaqirmaydi.
To'liq fayl: assets/js/render.js
js
// render.js — holatdan ekranga: chizish, fokus, e'lon
import { Vazifa } from "./vazifa.js";
import { holat, korinadiganlar } from "./holat.js";

export const boshMatnlar = Object.freeze({
  hammasi: "Hozircha vazifa yo'q. Birinchisini qo'shing!",
  faol: "Faol vazifa yo'q.",
  bajarilgan: "Bajarilgan vazifa hali yo'q.",
});

// Sahifa elementlari
export const forma = document.getElementById("yangi-forma");
export const maydon = document.getElementById("yangi-matn");
const maydonXato = document.getElementById("yangi-matn-xato");
export const filtrlar = document.getElementById("filtrlar");
const sarlavha = document.getElementById("royxat-sarlavha");
export const royxat = document.getElementById("royxat");
const boshXabar = document.getElementById("bosh-xabar");
const hisob = document.getElementById("hisob");
const teglarQatori = document.getElementById("teglar");
const elon = document.getElementById("elon");
export const eksportTugma = document.getElementById("eksport-tugma");
export const eksportMatn = document.getElementById("eksport-matn");
export const importMatn = document.getElementById("import-matn");
export const importTugma = document.getElementById("import-tugma");
const importXato = document.getElementById("import-xato");

function tugmaYasa(amal, matn, nomi) {
  const tugma = document.createElement("button");
  tugma.type = "button";
  tugma.dataset.amal = amal;
  tugma.textContent = matn;
  tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
  return tugma;
}

// Amal nomi va tugma matni — bir joyda, juftlik bilan
const tahrirTugmasi = tugmaYasa.bind(null, "tahrirla", "Tahrirlash");
const ochirTugmasi = tugmaYasa.bind(null, "ochir", "O'chirish");
const saqlaTugmasi = tugmaYasa.bind(null, "saqla", "Saqlash");
const bekorTugmasi = tugmaYasa.bind(null, "bekor", "Bekor qilish");

function oddiyQator(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  li.classList.toggle("bajarildi", vazifa.bajarildi);
  const katak = document.createElement("input");
  katak.type = "checkbox";
  katak.id = `vazifa-${vazifa.id}`;
  katak.dataset.amal = "almashtir";
  katak.checked = vazifa.bajarildi;
  const yorliq = document.createElement("label");
  yorliq.htmlFor = katak.id;
  yorliq.textContent = vazifa.matn;
  li.append(
    katak,
    yorliq,
    tahrirTugmasi(vazifa.matn),
    ochirTugmasi(vazifa.matn),
  );
  return li;
}

function tahrirQatori(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  const kiritma = document.createElement("input");
  kiritma.id = "tahrir-maydon";
  kiritma.value = vazifa.matn;
  kiritma.maxLength = Vazifa.MAKS_UZUNLIK;
  kiritma.setAttribute("aria-label", "Vazifa matni");
  li.append(
    kiritma,
    saqlaTugmasi(vazifa.matn),
    bekorTugmasi(vazifa.matn),
  );
  return li;
}

function qatorYasa(vazifa) {
  if (vazifa.id === holat.tahrirId) {
    return tahrirQatori(vazifa);
  }
  return oddiyQator(vazifa);
}

function matnniYoz(element, matn) {
  if (element.textContent !== matn) {
    element.textContent = matn;
  }
}

function teglarMatni(sanoq) {
  return [...sanoq]
    .map(([teg, soni]) => `${teg} — ${soni}`)
    .join(", ");
}

function teglarniYoz() {
  const sanoq = holat.vazifalar.tegHisobi();
  const yopilgan = [...holat.vazifalar.yopilganTeglar()];
  const qoshimcha =
    yopilgan.length > 0 ? `. Yopilgan: ${yopilgan.join(", ")}` : "";
  teglarQatori.hidden = sanoq.size === 0;
  matnniYoz(
    teglarQatori,
    `Teglar: ${teglarMatni(sanoq)}${qoshimcha}`,
  );
}

export function render() {
  royxat.replaceChildren(...korinadiganlar().map(qatorYasa));

  boshXabar.hidden = royxat.children.length > 0;
  matnniYoz(boshXabar, boshMatnlar[holat.filtr]);
  filtrlar.querySelectorAll("[data-filtr]").forEach((tugma) => {
    const tanlangan = tugma.dataset.filtr === holat.filtr;
    tugma.setAttribute("aria-pressed", String(tanlangan));
  });
  const { qoldi, bajarildi } = holat.vazifalar.statistika();
  matnniYoz(
    hisob,
    `${qoldi} ta vazifa qoldi, ${bajarildi} ta bajarildi`,
  );
  teglarniYoz();
}

// Fokus va e'lon: render'dan keyin
export function fokusla(id, amal, zaxiraId) {
  // querySelector — royxat'ning metodi: uni nuqta bilan chaqiramiz
  const top = (i) =>
    royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
  const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
  nishon.focus();
}

export function qoshniId(id) {
  const qator = royxat.querySelector(`[data-id="${id}"]`);
  const qoshni =
    qator.nextElementSibling ?? qator.previousElementSibling;
  return qoshni === null ? null : Number(qoshni.dataset.id);
}

export function elonQil(matn) {
  elon.textContent = matn;
}

export function xatoniKorsat(xabar) {
  maydonXato.textContent = xabar;
  maydon.setAttribute("aria-invalid", "true");
  maydon.setAttribute("aria-describedby", maydonXato.id);
}

export function xatoniTozala() {
  maydonXato.textContent = "";
  maydon.removeAttribute("aria-invalid");
  maydon.removeAttribute("aria-describedby");
}

export function tahrirgaOt() {
  const kiritma = document.getElementById("tahrir-maydon");
  kiritma.focus();
  kiritma.select();
}

export function importXatosiniKorsat(xabar) {
  importXato.textContent = xabar;
  importMatn.setAttribute("aria-invalid", "true");
  importMatn.focus();
}

export function importXatosiniTozala() {
  importXato.textContent = "";
  importMatn.removeAttribute("aria-invalid");
}

9.2 asosiy.js — kirish nuqtasi

Kirish nuqtasi hodisalarni ulaydi va ilovani boshlaydi. Asosiy qarorlar:

  • royxatniAlmashtir — yangi royxatga qayta obuna. VazifalarRoyxati.dan yangi obyekt yasaydi, eski royxatning obunachilari unga ko'chmaydi. obuna(render) qatorini unutsangiz, import ishlaydi, lekin keyingi bosishlar ekranni yangilamaydi — jim xato.
  • Nega royxatniAlmashtir holat.js da emas? Unga render kerak. holat.js render.js ni import qilsa, aylana paydo bo'ladi — Modul semantikasi darsidagi TDZ tuzog'i. filtrniTanla ham shu sababli asosiy.js da.
  • filtrniTanla — Object.hasOwn bilan (Object.* metodlari). "constructor" yoki "toString" — boshMatnlar ning meros kaliti, filtr emas (Prototip zanjiri).
  • Import tinglovchisi instanceof VazifaXatosi bilan o'z xatosini ajratadi. Begona xato — bug: foydalanuvchiga umumiy xabar, Console'ga esa qayta tashlanadi.
To'liq fayl: assets/js/asosiy.js
js
// asosiy.js — kirish nuqtasi: hodisalarni ulaydi, ilovani boshlaydi
import { VazifaXatosi } from "./vazifa.js";
import { VazifalarRoyxati } from "./royxat.js";
import { eksportMatni, paketniOqi } from "./paket.js";
import {
  holat,
  xatoniTop,
  vazifaniTop,
  vazifaQosh,
  vazifaniAlmashtir,
  vazifaniOchir,
  matnniAlmashtir,
} from "./holat.js";
import {
  boshMatnlar,
  forma,
  maydon,
  royxat,
  filtrlar,
  eksportTugma,
  eksportMatn,
  importMatn,
  importTugma,
  render,
  fokusla,
  qoshniId,
  elonQil,
  xatoniKorsat,
  xatoniTozala,
  tahrirgaOt,
  importXatosiniKorsat,
  importXatosiniTozala,
} from "./render.js";

// Holat + chizish: royxatga tegmaydigan o'zgarishlar
function tahrirniBoshla(id) {
  holat.tahrirId = id;
  render();
}

function tahrirniYop() {
  holat.tahrirId = null;
  render();
}

function filtrniTanla(filtr) {
  // Faqat boshMatnlar ning O'Z kalitlari — "constructor" filtr emas
  if (!Object.hasOwn(boshMatnlar, filtr)) {
    return;
  }
  holat.filtr = filtr;
  holat.tahrirId = null;
  render();
}

function royxatniAlmashtir(malumotlar) {
  holat.vazifalar = VazifalarRoyxati.dan(malumotlar);
  holat.vazifalar.obuna(render);
  holat.filtr = "hammasi";
  holat.tahrirId = null;
  render();
}

function tahrirniSaqla() {
  const kiritma = document.getElementById("tahrir-maydon");
  const id = holat.tahrirId;
  const matn = kiritma.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, id);
  if (xato !== null) {
    kiritma.setAttribute("aria-invalid", "true");
    kiritma.focus();
    elonQil(xato);
    return;
  }
  matnniAlmashtir(id, matn);
  fokusla(id, "tahrirla", null);
  elonQil("O'zgarish saqlandi.");
}

function tahrirniBekorQil() {
  const id = holat.tahrirId;
  tahrirniYop();
  fokusla(id, "tahrirla", null);
}

// Hodisalar: har joyda bitta tinglovchi
forma.addEventListener("submit", (event) => {
  event.preventDefault();
  elonQil("");
  const matn = maydon.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, null);
  if (xato !== null) {
    xatoniKorsat(xato);
    maydon.focus();
    return;
  }
  xatoniTozala();
  vazifaQosh(matn);
  forma.reset();
  elonQil(`«${matn}» qo'shildi.`);
});

maydon.addEventListener("input", () => {
  if (maydon.getAttribute("aria-invalid") !== "true") {
    return;
  }
  const matn = maydon.value.trim();
  if (xatoniTop(matn, holat.vazifalar, null) === null) {
    xatoniTozala();
  }
});

royxat.addEventListener("click", (event) => {
  const nishon = event.target.closest("[data-amal]");
  if (nishon === null || !royxat.contains(nishon)) {
    return;
  }
  elonQil("");
  const amal = nishon.dataset.amal;
  const id = Number(nishon.closest("li").dataset.id);
  const zaxira = qoshniId(id);
  if (amal !== "saqla" && amal !== "bekor") {
    holat.tahrirId = null;
  }
  switch (amal) {
    case "almashtir":
      vazifaniAlmashtir(id);
      fokusla(id, "almashtir", zaxira);
      break;
    case "ochir":
      elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
      vazifaniOchir(id);
      fokusla(zaxira, "ochir", null);
      break;
    case "tahrirla":
      tahrirniBoshla(id);
      tahrirgaOt();
      break;
    case "saqla":
      tahrirniSaqla();
      break;
    case "bekor":
      tahrirniBekorQil();
      break;
  }
});

royxat.addEventListener("keydown", (event) => {
  if (event.target.id !== "tahrir-maydon" || event.isComposing) {
    return;
  }
  if (event.key === "Enter") {
    event.preventDefault();
    tahrirniSaqla();
  } else if (event.key === "Escape") {
    tahrirniBekorQil();
  }
});

filtrlar.addEventListener("click", (event) => {
  const tugma = event.target.closest("[data-filtr]");
  if (tugma === null) {
    return;
  }
  elonQil("");
  filtrniTanla(tugma.dataset.filtr);
});

eksportTugma.addEventListener("click", () => {
  eksportMatn.value = eksportMatni(holat.vazifalar);
  eksportMatn.focus();
  eksportMatn.select();
  elonQil("JSON tayyor. Nusxa olish uchun Ctrl+C bosing.");
});

importTugma.addEventListener("click", () => {
  importXatosiniTozala();
  elonQil("");
  try {
    const malumotlar = paketniOqi(importMatn.value);
    royxatniAlmashtir(malumotlar);
    elonQil(`${malumotlar.length} ta vazifa import qilindi.`);
  } catch (xato) {
    if (xato instanceof VazifaXatosi) {
      importXatosiniKorsat(xato.message);
      return;
    }
    importXatosiniKorsat("Kutilmagan xato: import bajarilmadi.");
    throw xato;
  }
});

// Ishga tushirish
holat.vazifalar.obuna(render);
render();

Bitta bosishning yo'li — katakni belgilash:

sequenceDiagram
  participant F as Foydalanuvchi
  participant A as asosiy.js
  participant H as holat.js
  participant RO as royxat.js
  participant R as render.js
  F->>A: katakni bosadi
  A->>H: vazifaniAlmashtir(id)
  H->>RO: almashtir(id)
  RO->>R: xabarBer: render()
  R-->>F: yangi ro'yxat va hisob
  A->>R: fokusla(id, ...)

Diagrammaga qarang: asosiy.js render() ni o'zi chaqirmaydi — royxat obunachisiga xabar beradi. fokusla esa render tugagandan keyin ishlaydi: kuzatuvchi sinxron.

Tekshirib ko'ring: Tinglovchida fokusla(id, "almashtir", zaxira) ni vazifaniAlmashtir(id) dan oldin yozsak, nima bo'ladi?

Javob

Fokus avval eski katakka qo'yiladi. Keyin royxat o'zgaradi, obunachi render() ni chaqiradi va replaceChildren eski qatorlarni o'chirib, yangilarini yasaydi. Fokusdagi element sahifadan chiqib ketadi — fokus body ga tushadi. Shuning uchun qoida 09-qismdagidek: avval holat va chizish, keyin fokus.

9.3 index.html

09-qismdagi sahifaga qo'shilganlar: #teglar qatori, eksport va import bo'limlari, beshta modul uchun modulepreload va bitta kirish nuqtasi (Brauzerda ESM). Kirish nuqtasini ulaydigan <script> tegining type atributi — .

To'liq fayl: index.html
html
<!DOCTYPE html>
<html lang="uz">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Vazifalar — kichik vazifalar ro'yxati</title>
  <meta name="description"
        content="Vanilla JavaScript'da yozilgan vazifalar ro'yxati.">
  <link rel="stylesheet" href="assets/css/vazifalar.css">
  <link rel="modulepreload" href="assets/js/holat.js">
  <link rel="modulepreload" href="assets/js/render.js">
  <link rel="modulepreload" href="assets/js/royxat.js">
  <link rel="modulepreload" href="assets/js/paket.js">
  <link rel="modulepreload" href="assets/js/vazifa.js">
  <script type="module" src="assets/js/asosiy.js"></script>
</head>
<body>
  <main>
    <h1>Vazifalar</h1>
    <form id="yangi-forma" novalidate>
      <label for="yangi-matn">Yangi vazifa</label>
      <div class="qator">
        <input id="yangi-matn" name="matn" required
               maxlength="100" autocomplete="off">
        <button type="submit">Qo'shish</button>
      </div>
      <p id="yangi-matn-xato" class="xato" role="status"></p>
    </form>
    <div id="filtrlar" role="group" aria-label="Filtr">
      <button type="button" data-filtr="hammasi"
              aria-pressed="true">Hammasi</button>
      <button type="button" data-filtr="faol"
              aria-pressed="false">Faol</button>
      <button type="button" data-filtr="bajarilgan"
              aria-pressed="false">Bajarilgan</button>
    </div>
    <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
    <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
    <p id="bosh-xabar" hidden></p>
    <p id="hisob" aria-live="polite"></p>
    <p id="teglar" hidden></p>
    <p id="elon" role="status"></p>
    <h2>Eksport</h2>
    <button type="button"
            id="eksport-tugma">JSON qilib chiqarish</button>
    <label for="eksport-matn">Vazifalar JSON ko'rinishida</label>
    <textarea id="eksport-matn" rows="6" readonly></textarea>
    <h2>Import</h2>
    <label for="import-matn">Eksport qilingan JSON matni</label>
    <textarea id="import-matn" rows="6"></textarea>
    <button type="button" id="import-tugma">Import qilish</button>
    <p id="import-xato" class="xato" aria-live="polite"></p>
    <p><small>v2: sahifa yangilansa, ro'yxat boshidagi
      holatiga qaytadi — saqlash 11-qismda.</small></p>
  </main>
</body>
</html>

Tekshiruv — endi brauzerda. Live Server'da oching va tekshiruv/ssenariy.md dagi 14 qadamni bajaring. Console toza bo'lishi kerak. Unda typeof holat deb yozing — 'undefined': modul nomlari global'ga chiqmaydi (Global obyekt).

9.4 Hammasi bitta oynada — jonli sinov

Jonli oyna ham bir nechta faylni ocha olmaydi (Brauzerda ESM). Shuning uchun oltita modulni bitta <script type="module"> ga yig'dik: import/export olib tashlangan, tartib — grafik bo'yicha pastdan yuqoriga. Bu faqat sinash uchun — loyihangizda fayllar alohida turadi. Oxirgi uch qator — // DEBUG belgili sinov qatorlari:

html
<style>
/* vazifalar.css — vazifalar ro'yxati v2 */
:root {
  --rang-matn: #1f2a24;
  --rang-fon: #f7faf8;
  --rang-asosiy: #1f6f4a;
  --rang-chegara: #c9d6cf;
  --rang-ikkinchi: #5b6660;
  --rang-xato: #b3261e;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font: 1rem/1.5 system-ui, sans-serif;
  color: var(--rang-matn);
  background: var(--rang-fon);
}
main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
h1 { margin-top: 0; color: var(--rang-asosiy); }
h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
button, input, textarea { font: inherit; }
button {
  min-height: 2.75rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--rang-chegara);
  border-radius: 0.5rem;
  background: #fff;
  color: var(--rang-matn);
}
button[aria-pressed="true"] {
  border-color: var(--rang-asosiy);
  background: var(--rang-asosiy);
  color: #fff;
}
:focus-visible {
  outline: 3px solid var(--rang-asosiy);
  outline-offset: 2px;
}
.qator { display: flex; gap: 0.5rem; }
#yangi-matn, #tahrir-maydon {
  flex: 1;
  min-width: 0;
  min-height: 2.75rem;
  padding: 0.5rem;
  border: 2px solid var(--rang-ikkinchi);
  border-radius: 0.5rem;
}
[aria-invalid="true"] { border-color: var(--rang-xato); }
.xato {
  min-height: 1.5em;
  margin: 0.25rem 0 0;
  color: var(--rang-xato);
}
#filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
#royxat { margin: 0; padding: 0; list-style: none; }
#royxat li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--rang-chegara);
}
#royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
#royxat [type="checkbox"] {
  width: 1.25rem;
  height: 1.25rem;
}
.bajarildi label {
  color: var(--rang-ikkinchi);
  text-decoration: line-through;
}
#hisob { font-weight: bold; }
textarea {
  display: block;
  width: 100%;
  margin-block: 0.25rem 0.5rem;
  padding: 0.5rem;
  border: 2px solid var(--rang-ikkinchi);
  border-radius: 0.5rem;
}
</style>
<main>
  <h1>Vazifalar</h1>
  <form id="yangi-forma" novalidate>
    <label for="yangi-matn">Yangi vazifa</label>
    <div class="qator">
      <input id="yangi-matn" name="matn" required
             maxlength="100" autocomplete="off">
      <button type="submit">Qo'shish</button>
    </div>
    <p id="yangi-matn-xato" class="xato" role="status"></p>
  </form>
  <div id="filtrlar" role="group" aria-label="Filtr">
    <button type="button" data-filtr="hammasi"
            aria-pressed="true">Hammasi</button>
    <button type="button" data-filtr="faol"
            aria-pressed="false">Faol</button>
    <button type="button" data-filtr="bajarilgan"
            aria-pressed="false">Bajarilgan</button>
  </div>
  <h2 id="royxat-sarlavha" tabindex="-1">Ro'yxat</h2>
  <ul id="royxat" aria-labelledby="royxat-sarlavha"></ul>
  <p id="bosh-xabar" hidden></p>
  <p id="hisob" aria-live="polite"></p>
  <p id="teglar" hidden></p>
  <p id="elon" role="status"></p>
  <h2>Eksport</h2>
  <button type="button"
          id="eksport-tugma">JSON qilib chiqarish</button>
  <label for="eksport-matn">Vazifalar JSON ko'rinishida</label>
  <textarea id="eksport-matn" rows="6" readonly></textarea>
  <h2>Import</h2>
  <label for="import-matn">Eksport qilingan JSON matni</label>
  <textarea id="import-matn" rows="6"></textarea>
  <button type="button" id="import-tugma">Import qilish</button>
  <p id="import-xato" class="xato" aria-live="polite"></p>
  <p><small>v2: sahifa yangilansa, ro'yxat boshidagi
    holatiga qaytadi — saqlash 11-qismda.</small></p>
</main>
<script type="module">
// vazifalar v2 — olti modul bitta oynada (faqat jonli namuna)

// vazifa.js — bitta vazifa va vazifalar xatosi

class VazifaXatosi extends Error {
  constructor(xabar, sozlama) {
    super(xabar, sozlama);
    this.name = "VazifaXatosi";
    this.kod = sozlama?.kod ?? "NOMALUM";
  }
}

class Vazifa {
  // index.html dagi maxlength bilan bir xil bo'lsin
  static MAKS_UZUNLIK = 100;

  id;
  bajarildi;
  #matn;

  constructor(id, matn, bajarildi = false) {
    this.id = id;
    this.#matn = matn;
    this.bajarildi = bajarildi;
  }

  static dan(malumot) {
    return new Vazifa(malumot.id, malumot.matn, malumot.bajarildi);
  }

  get matn() {
    return this.#matn;
  }

  matnniOzgartir(yangi) {
    const toza = yangi.trim();
    if (toza === "" || toza.length > Vazifa.MAKS_UZUNLIK) {
      return false;
    }
    this.#matn = toza;
    return true;
  }

  almashtir() {
    this.bajarildi = !this.bajarildi;
  }

  toJSON() {
    return {
      id: this.id,
      matn: this.#matn,
      bajarildi: this.bajarildi,
    };
  }
}

// royxat.js — vazifalar to'plami: so'rovlar, o'zgartirishlar, obuna

// Quyidagi beshtasi eksport qilinmaydi — faqat shu modul ichida
function idYasovchiYarat(boshlangich) {
  let keyingi = boshlangich;
  return () => keyingi++;
}

function faollarOldin(a, b) {
  return Number(a.bajarildi) - Number(b.bajarildi);
}

function guruhNomi(vazifa) {
  return vazifa.bajarildi ? "bajarilgan" : "faol";
}

function teglariniOl(matn) {
  return new Set(
    matn
      .toLowerCase()
      .split(" ")
      .filter((soz) => soz.length > 1 && soz.startsWith("#")),
  );
}

function tegToplami(vazifalar) {
  return new Set(
    vazifalar.flatMap((vazifa) => [...teglariniOl(vazifa.matn)]),
  );
}

class VazifalarRoyxati {
  #vazifalar = [];
  #yangiId = idYasovchiYarat(1);
  #tinglovchilar = [];

  static dan(malumotlar) {
    const yangi = new VazifalarRoyxati();
    yangi.#vazifalar = malumotlar.map((m) => Vazifa.dan(m));
    const engKatta = malumotlar.reduce(
      (eng, m) => Math.max(eng, m.id),
      0,
    );
    yangi.#yangiId = idYasovchiYarat(engKatta + 1);
    return yangi;
  }

  static mosmi(vazifa, filtr) {
    if (filtr === "faol") {
      return !vazifa.bajarildi;
    }
    if (filtr === "bajarilgan") {
      return vazifa.bajarildi;
    }
    return true;
  }

  // So'rovlar: royxatni o'zgartirmaydi
  statistika() {
    const guruhlar = Object.groupBy(this.#vazifalar, guruhNomi);
    return {
      qoldi: guruhlar.faol?.length ?? 0,
      bajarildi: guruhlar.bajarilgan?.length ?? 0,
    };
  }

  get qoldi() {
    return this.statistika().qoldi;
  }

  korinadiganlar(filtr) {
    return this.#vazifalar
      .filter((v) => VazifalarRoyxati.mosmi(v, filtr))
      .toSorted(faollarOldin);
  }

  top(id) {
    return this.#vazifalar.find((v) => v.id === id);
  }

  matnBormi(matn, ozId) {
    const kichik = matn.toLowerCase();
    return this.#vazifalar.some(
      (v) => v.id !== ozId && v.matn.toLowerCase() === kichik,
    );
  }

  tegHisobi() {
    return this.#vazifalar
      .flatMap((vazifa) => [...teglariniOl(vazifa.matn)])
      .reduce(
        (sanoq, teg) => sanoq.set(teg, (sanoq.get(teg) ?? 0) + 1),
        new Map(),
      );
  }

  yopilganTeglar() {
    const bajarilgan = this.#vazifalar.filter((v) => v.bajarildi);
    const faol = this.#vazifalar.filter((v) => !v.bajarildi);
    return tegToplami(bajarilgan).difference(tegToplami(faol));
  }

  // Kuzatuvchi: royxat o'zgarsa, obunachilarga xabar
  obuna(tinglovchi) {
    this.#tinglovchilar.push(tinglovchi);
    return () => {
      this.#tinglovchilar = this.#tinglovchilar.filter(
        (t) => t !== tinglovchi,
      );
    };
  }

  #xabarBer() {
    this.#tinglovchilar.forEach((tinglovchi) => tinglovchi());
  }

  // O'zgartiruvchilar: har biri oxirida #xabarBer()
  qosh(matn) {
    const vazifa = new Vazifa(this.#yangiId(), matn);
    this.#vazifalar.push(vazifa);
    this.#xabarBer();
    return vazifa;
  }

  almashtir(id) {
    this.top(id).almashtir();
    this.#xabarBer();
  }

  matnniOzgartir(id, matn) {
    const ozgardi = this.top(id).matnniOzgartir(matn);
    this.#xabarBer();
    return ozgardi;
  }

  ochir(id) {
    this.#vazifalar = this.#vazifalar.filter((v) => v.id !== id);
    this.#xabarBer();
  }

  *[Symbol.iterator]() {
    yield* this.#vazifalar;
  }

  toJSON() {
    return this.#vazifalar.map((v) => v.toJSON());
  }
}

// paket.js — eksport va import: {versiya: 1, vazifalar: [...]}

const VAZIFA_MAYDONLARI = ["id", "matn", "bajarildi"];
const eksportMaydonlari = [
  "versiya", "vazifalar", ...VAZIFA_MAYDONLARI,
];

function eksportMatni(vazifalar) {
  const paket = { versiya: 1, vazifalar };
  return JSON.stringify(paket, eksportMaydonlari, 2);
}

function oddiyObyektmi(qiymat) {
  return typeof qiymat === "object" && qiymat !== null
    && !Array.isArray(qiymat);
}

// Oq ro'yxat: faqat uch maydon va faqat o'z kalitlari
function vazifaniOl(xom) {
  const vazifa = {};
  if (!oddiyObyektmi(xom)) {
    return vazifa;
  }
  for (const maydon of VAZIFA_MAYDONLARI) {
    if (Object.hasOwn(xom, maydon)) {
      vazifa[maydon] = xom[maydon];
    }
  }
  return vazifa;
}

function vazifaXatosi(tartib, sabab) {
  return new VazifaXatosi(`${tartib}-vazifa: ${sabab}`, {
    kod: "VAZIFA",
  });
}

function malumotlarniTekshir(malumotlar) {
  const idlar = new Set();
  malumotlar.forEach((v, i) => {
    const tartib = i + 1;
    if (!Number.isInteger(v.id) || v.id < 1 || idlar.has(v.id)) {
      throw vazifaXatosi(tartib, "id musbat, takrorlanmas son emas.");
    }
    idlar.add(v.id);
    if (typeof v.matn !== "string" || v.matn.trim() === "") {
      throw vazifaXatosi(tartib, "matn bo'sh.");
    }
    if (v.matn.length > Vazifa.MAKS_UZUNLIK) {
      throw vazifaXatosi(tartib, "matn juda uzun.");
    }
    if (typeof v.bajarildi !== "boolean") {
      throw vazifaXatosi(tartib, "bajarildi true yoki false emas.");
    }
  });
}

function paketniOqi(matn) {
  let xom;
  try {
    xom = JSON.parse(matn);
  } catch (xato) {
    throw new VazifaXatosi(
      "Matn JSON emas. Eksportni to'liq oling.",
      { kod: "JSON", cause: xato },
    );
  }
  if (!oddiyObyektmi(xom)) {
    throw new VazifaXatosi("Bu matn vazifalar eksporti emas.", {
      kod: "TUZILMA",
    });
  }
  if (xom.versiya !== 1) {
    throw new VazifaXatosi(
      `Versiya ${xom.versiya} o'qilmaydi — faqat 1.`,
      { kod: "VERSIYA" },
    );
  }
  if (!Array.isArray(xom.vazifalar)) {
    throw new VazifaXatosi("Matnda vazifalar ro'yxati yo'q.", {
      kod: "TUZILMA",
    });
  }
  const malumotlar = xom.vazifalar.map(vazifaniOl);
  malumotlarniTekshir(malumotlar);
  return malumotlar;
}

// holat.js — ilova holati: yagona haqiqat manbai

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

const boshlangichVazifalar = chuqurMuzlat([
  { id: 1, matn: "Non olish", bajarildi: false },
  { id: 2, matn: "DOM darsini takrorlash", bajarildi: true },
  { id: 3, matn: "Onamga qo'ng'iroq qilish", bajarildi: false },
]);

const holat = {
  vazifalar: VazifalarRoyxati.dan(boshlangichVazifalar),
  filtr: "hammasi",
  tahrirId: null,
};

// So'rovlar: faqat qiymat qaytaradi
function xatoniTop(matn, vazifalar, ozId) {
  if (matn === "") {
    return "Vazifa matnini yozing.";
  }
  if (vazifalar.matnBormi(matn, ozId)) {
    return "Bu vazifa ro'yxatda allaqachon bor.";
  }
  return null;
}

function korinadiganlar() {
  return holat.vazifalar.korinadiganlar(holat.filtr);
}

function vazifaniTop(id) {
  return holat.vazifalar.top(id);
}

// O'zgartiruvchilar: avval holat, oxirida royxat (u render'ni
// kuzatuvchi orqali chaqiradi)
function vazifaQosh(matn) {
  if (holat.filtr === "bajarilgan") {
    holat.filtr = "hammasi";
  }
  holat.vazifalar.qosh(matn);
}

function vazifaniAlmashtir(id) {
  holat.vazifalar.almashtir(id);
}

function vazifaniOchir(id) {
  holat.vazifalar.ochir(id);
}

function matnniAlmashtir(id, matn) {
  holat.tahrirId = null;
  holat.vazifalar.matnniOzgartir(id, matn);
}

// render.js — holatdan ekranga: chizish, fokus, e'lon

const boshMatnlar = Object.freeze({
  hammasi: "Hozircha vazifa yo'q. Birinchisini qo'shing!",
  faol: "Faol vazifa yo'q.",
  bajarilgan: "Bajarilgan vazifa hali yo'q.",
});

// Sahifa elementlari
const forma = document.getElementById("yangi-forma");
const maydon = document.getElementById("yangi-matn");
const maydonXato = document.getElementById("yangi-matn-xato");
const filtrlar = document.getElementById("filtrlar");
const sarlavha = document.getElementById("royxat-sarlavha");
const royxat = document.getElementById("royxat");
const boshXabar = document.getElementById("bosh-xabar");
const hisob = document.getElementById("hisob");
const teglarQatori = document.getElementById("teglar");
const elon = document.getElementById("elon");
const eksportTugma = document.getElementById("eksport-tugma");
const eksportMatn = document.getElementById("eksport-matn");
const importMatn = document.getElementById("import-matn");
const importTugma = document.getElementById("import-tugma");
const importXato = document.getElementById("import-xato");

function tugmaYasa(amal, matn, nomi) {
  const tugma = document.createElement("button");
  tugma.type = "button";
  tugma.dataset.amal = amal;
  tugma.textContent = matn;
  tugma.setAttribute("aria-label", `${matn}: ${nomi}`);
  return tugma;
}

// Amal nomi va tugma matni — bir joyda, juftlik bilan
const tahrirTugmasi = tugmaYasa.bind(null, "tahrirla", "Tahrirlash");
const ochirTugmasi = tugmaYasa.bind(null, "ochir", "O'chirish");
const saqlaTugmasi = tugmaYasa.bind(null, "saqla", "Saqlash");
const bekorTugmasi = tugmaYasa.bind(null, "bekor", "Bekor qilish");

function oddiyQator(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  li.classList.toggle("bajarildi", vazifa.bajarildi);
  const katak = document.createElement("input");
  katak.type = "checkbox";
  katak.id = `vazifa-${vazifa.id}`;
  katak.dataset.amal = "almashtir";
  katak.checked = vazifa.bajarildi;
  const yorliq = document.createElement("label");
  yorliq.htmlFor = katak.id;
  yorliq.textContent = vazifa.matn;
  li.append(
    katak,
    yorliq,
    tahrirTugmasi(vazifa.matn),
    ochirTugmasi(vazifa.matn),
  );
  return li;
}

function tahrirQatori(vazifa) {
  const li = document.createElement("li");
  li.dataset.id = vazifa.id;
  const kiritma = document.createElement("input");
  kiritma.id = "tahrir-maydon";
  kiritma.value = vazifa.matn;
  kiritma.maxLength = Vazifa.MAKS_UZUNLIK;
  kiritma.setAttribute("aria-label", "Vazifa matni");
  li.append(
    kiritma,
    saqlaTugmasi(vazifa.matn),
    bekorTugmasi(vazifa.matn),
  );
  return li;
}

function qatorYasa(vazifa) {
  if (vazifa.id === holat.tahrirId) {
    return tahrirQatori(vazifa);
  }
  return oddiyQator(vazifa);
}

function matnniYoz(element, matn) {
  if (element.textContent !== matn) {
    element.textContent = matn;
  }
}

function teglarMatni(sanoq) {
  return [...sanoq]
    .map(([teg, soni]) => `${teg} — ${soni}`)
    .join(", ");
}

function teglarniYoz() {
  const sanoq = holat.vazifalar.tegHisobi();
  const yopilgan = [...holat.vazifalar.yopilganTeglar()];
  const qoshimcha =
    yopilgan.length > 0 ? `. Yopilgan: ${yopilgan.join(", ")}` : "";
  teglarQatori.hidden = sanoq.size === 0;
  matnniYoz(
    teglarQatori,
    `Teglar: ${teglarMatni(sanoq)}${qoshimcha}`,
  );
}

function render() {
  royxat.replaceChildren(...korinadiganlar().map(qatorYasa));

  boshXabar.hidden = royxat.children.length > 0;
  matnniYoz(boshXabar, boshMatnlar[holat.filtr]);
  filtrlar.querySelectorAll("[data-filtr]").forEach((tugma) => {
    const tanlangan = tugma.dataset.filtr === holat.filtr;
    tugma.setAttribute("aria-pressed", String(tanlangan));
  });
  const { qoldi, bajarildi } = holat.vazifalar.statistika();
  matnniYoz(
    hisob,
    `${qoldi} ta vazifa qoldi, ${bajarildi} ta bajarildi`,
  );
  teglarniYoz();
}

// Fokus va e'lon: render'dan keyin
function fokusla(id, amal, zaxiraId) {
  // querySelector — royxat'ning metodi: uni nuqta bilan chaqiramiz
  const top = (i) =>
    royxat.querySelector(`[data-id="${i}"] [data-amal="${amal}"]`);
  const nishon = top(id) ?? top(zaxiraId) ?? sarlavha;
  nishon.focus();
}

function qoshniId(id) {
  const qator = royxat.querySelector(`[data-id="${id}"]`);
  const qoshni =
    qator.nextElementSibling ?? qator.previousElementSibling;
  return qoshni === null ? null : Number(qoshni.dataset.id);
}

function elonQil(matn) {
  elon.textContent = matn;
}

function xatoniKorsat(xabar) {
  maydonXato.textContent = xabar;
  maydon.setAttribute("aria-invalid", "true");
  maydon.setAttribute("aria-describedby", maydonXato.id);
}

function xatoniTozala() {
  maydonXato.textContent = "";
  maydon.removeAttribute("aria-invalid");
  maydon.removeAttribute("aria-describedby");
}

function tahrirgaOt() {
  const kiritma = document.getElementById("tahrir-maydon");
  kiritma.focus();
  kiritma.select();
}

function importXatosiniKorsat(xabar) {
  importXato.textContent = xabar;
  importMatn.setAttribute("aria-invalid", "true");
  importMatn.focus();
}

function importXatosiniTozala() {
  importXato.textContent = "";
  importMatn.removeAttribute("aria-invalid");
}

// asosiy.js — kirish nuqtasi: hodisalarni ulaydi, ilovani boshlaydi

// Holat + chizish: royxatga tegmaydigan o'zgarishlar
function tahrirniBoshla(id) {
  holat.tahrirId = id;
  render();
}

function tahrirniYop() {
  holat.tahrirId = null;
  render();
}

function filtrniTanla(filtr) {
  // Faqat boshMatnlar ning O'Z kalitlari — "constructor" filtr emas
  if (!Object.hasOwn(boshMatnlar, filtr)) {
    return;
  }
  holat.filtr = filtr;
  holat.tahrirId = null;
  render();
}

function royxatniAlmashtir(malumotlar) {
  holat.vazifalar = VazifalarRoyxati.dan(malumotlar);
  holat.vazifalar.obuna(render);
  holat.filtr = "hammasi";
  holat.tahrirId = null;
  render();
}

function tahrirniSaqla() {
  const kiritma = document.getElementById("tahrir-maydon");
  const id = holat.tahrirId;
  const matn = kiritma.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, id);
  if (xato !== null) {
    kiritma.setAttribute("aria-invalid", "true");
    kiritma.focus();
    elonQil(xato);
    return;
  }
  matnniAlmashtir(id, matn);
  fokusla(id, "tahrirla", null);
  elonQil("O'zgarish saqlandi.");
}

function tahrirniBekorQil() {
  const id = holat.tahrirId;
  tahrirniYop();
  fokusla(id, "tahrirla", null);
}

// Hodisalar: har joyda bitta tinglovchi
forma.addEventListener("submit", (event) => {
  event.preventDefault();
  elonQil("");
  const matn = maydon.value.trim();
  const xato = xatoniTop(matn, holat.vazifalar, null);
  if (xato !== null) {
    xatoniKorsat(xato);
    maydon.focus();
    return;
  }
  xatoniTozala();
  vazifaQosh(matn);
  forma.reset();
  elonQil(`«${matn}» qo'shildi.`);
});

maydon.addEventListener("input", () => {
  if (maydon.getAttribute("aria-invalid") !== "true") {
    return;
  }
  const matn = maydon.value.trim();
  if (xatoniTop(matn, holat.vazifalar, null) === null) {
    xatoniTozala();
  }
});

royxat.addEventListener("click", (event) => {
  const nishon = event.target.closest("[data-amal]");
  if (nishon === null || !royxat.contains(nishon)) {
    return;
  }
  elonQil("");
  const amal = nishon.dataset.amal;
  const id = Number(nishon.closest("li").dataset.id);
  const zaxira = qoshniId(id);
  if (amal !== "saqla" && amal !== "bekor") {
    holat.tahrirId = null;
  }
  switch (amal) {
    case "almashtir":
      vazifaniAlmashtir(id);
      fokusla(id, "almashtir", zaxira);
      break;
    case "ochir":
      elonQil(`«${vazifaniTop(id).matn}» o'chirildi.`);
      vazifaniOchir(id);
      fokusla(zaxira, "ochir", null);
      break;
    case "tahrirla":
      tahrirniBoshla(id);
      tahrirgaOt();
      break;
    case "saqla":
      tahrirniSaqla();
      break;
    case "bekor":
      tahrirniBekorQil();
      break;
  }
});

royxat.addEventListener("keydown", (event) => {
  if (event.target.id !== "tahrir-maydon" || event.isComposing) {
    return;
  }
  if (event.key === "Enter") {
    event.preventDefault();
    tahrirniSaqla();
  } else if (event.key === "Escape") {
    tahrirniBekorQil();
  }
});

filtrlar.addEventListener("click", (event) => {
  const tugma = event.target.closest("[data-filtr]");
  if (tugma === null) {
    return;
  }
  elonQil("");
  filtrniTanla(tugma.dataset.filtr);
});

eksportTugma.addEventListener("click", () => {
  eksportMatn.value = eksportMatni(holat.vazifalar);
  eksportMatn.focus();
  eksportMatn.select();
  elonQil("JSON tayyor. Nusxa olish uchun Ctrl+C bosing.");
});

importTugma.addEventListener("click", () => {
  importXatosiniTozala();
  elonQil("");
  try {
    const malumotlar = paketniOqi(importMatn.value);
    royxatniAlmashtir(malumotlar);
    elonQil(`${malumotlar.length} ta vazifa import qilindi.`);
  } catch (xato) {
    if (xato instanceof VazifaXatosi) {
      importXatosiniKorsat(xato.message);
      return;
    }
    importXatosiniKorsat("Kutilmagan xato: import bajarilmadi.");
    throw xato;
  }
});

// Ishga tushirish
holat.vazifalar.obuna(render);
render();

console.log(royxat.children); // DEBUG
console.log(hisob.textContent); // DEBUG
console.log([...holat.vazifalar].length); // DEBUG
</script>

Konsolda:

text
HTMLCollection(3) [li, li, li.bajarildi]
2 ta vazifa qoldi, 1 ta bajarildi
3

li.bajarildi oxirida — saralash ishlayapti. Oynada hozir sinab ko'rishingiz mumkin.

Uchta sinov — 10-qismda qo'shilgan imkoniyatlar uchun (yuqoridagi oynada yoki Live Server'da):

  • "Sabzi olish #bozor #uy" va "Guruch olish #Bozor" qo'shing — "Teglar: #bozor — 2, #uy — 1". Ikkalasini belgilang — "Yopilgan: #bozor, #uy".
  • «JSON qilib chiqarish» → matnni nusxalang → bitta vazifani belgilang → matnni «Import» maydoniga qo'yib, «Import qilish» — ro'yxat eksport paytiga qaytadi.
  • Import maydoniga {"versiya": 1, "vazifalar": [} — qizil xabar, fokus maydonda, ro'yxat joyida.

Biz bu ssenariyni va yana 20 dan ortiq qadamni (takror id, null, __proto__ li paket, "constructor" filtri) Chrome 154 da avtomatik sinov bilan o'tkazdik. Ro'yxat, hisob, teglar, e'lon va fokus har qadamda kutilgan natijaga mos keldi.

10. 7-bosqich: Node tekshiruvi

Mantiq qatlami DOM'siz — demak uni brauzer ochmasdan, bir buyruq bilan tekshirish mumkin. CommonJS va ESM darsidagi package.json hamon kerak — "type": "module" tufayli Node .js fayllarni ES modul deb o'qiydi:

json
{
  "name": "vazifalar",
  "private": true,
  "type": "module"
}

tekshiruv/royxat.js — CommonJS va ESM darsidagi faylni kengaytiramiz: endi royxat, holat va paketning asosiy va'dalari:

js
// tekshiruv/royxat.js — mantiq qatlamini brauzersiz tekshirish
// Ishga tushirish: node tekshiruv/royxat.js
import { VazifalarRoyxati } from "../assets/js/royxat.js";
import { eksportMatni, paketniOqi } from "../assets/js/paket.js";
import { boshlangichVazifalar } from "../assets/js/holat.js";

function tekshir(nomi, haqiqiy, kutilgan) {
  const belgi = haqiqiy === kutilgan ? "✅" : "❌";
  console.log(`${belgi} ${nomi}: ${haqiqiy}`);
}

const royxat = VazifalarRoyxati.dan(boshlangichVazifalar);
let chizildi = 0;
royxat.obuna(() => chizildi++);

const { qoldi, bajarildi } = royxat.statistika();
tekshir("statistika", `${qoldi}/${bajarildi}`, "2/1");
tekshir("yangi id", royxat.qosh("Sabzi olish #bozor").id, 4);
tekshir("takror matn", royxat.matnBormi("NON OLISH", null), true);
royxat.almashtir(1);
tekshir("faollar tepada", royxat.korinadiganlar("hammasi")
  .map((v) => v.id).join(","), "3,4,1,2");
tekshir("teglar", royxat.tegHisobi().get("#bozor"), 1);
royxat.ochir(4);
tekshir("o'chgan id qaytmaydi", royxat.qosh("Choy damlash").id, 5);
tekshir("kuzatuvchi", chizildi, 4);
tekshir("iterable", [...royxat].length, 4);
tekshir("muzlagan", Object.isFrozen(boshlangichVazifalar[0]), true);
tekshir("asl tegilmagan", boshlangichVazifalar[0].bajarildi, false);

const qaytgan = paketniOqi(eksportMatni(royxat));
tekshir("eksport → import", qaytgan.length, 4);
tekshir("matn saqlandi", qaytgan[3].matn, "Choy damlash");
bash
node tekshiruv/royxat.js
text
✅ statistika: 2/1
✅ yangi id: 4
✅ takror matn: true
✅ faollar tepada: 3,4,1,2
✅ teglar: 1
✅ o'chgan id qaytmaydi: 5
✅ kuzatuvchi: 4
✅ iterable: 4
✅ muzlagan: true
✅ asl tegilmagan: false
✅ eksport → import: 4
✅ matn saqlandi: Choy damlash

Uch qatorga e'tibor bering. "faollar tepada" — #vazifalar emas, ko'rinadigan tartib: belgilangan 1-vazifa pastga tushdi. "kuzatuvchi: 4" — to'rt o'zgartirish, to'rt xabar. "asl tegilmagan" — muzlagan boshlang'ich ma'lumot belgilashdan keyin ham false.

tekshiruv/paket.js — import qaysi matnni qanday rad etadi:

js
// tekshiruv/paket.js — import qaysi matnni qanday rad etadi
// Ishga tushirish: node tekshiruv/paket.js
import { paketniOqi } from "../assets/js/paket.js";

function xatoKodi(matn) {
  try {
    paketniOqi(matn);
    return "o'qildi";
  } catch (xato) {
    return `${xato.name} ${xato.kod}: ${xato.message}`;
  }
}

const v = (qator) => `{"versiya": 1, "vazifalar": [${qator}]}`;
const matnlar = [
  v('{"id": 7, "matn": "Choy", "bajarildi": false}'),
  '{"versiya": 1, "vazifalar": [}',
  '{"versiya": 2, "vazifalar": []}',
  "null",
  v('{"id": 1, "matn": "A", "bajarildi": "ha"}'),
  v('{"matn": "id siz", "bajarildi": true}'),
  '{"__proto__": {"admin": true}, "versiya": 1, "vazifalar": []}',
];

for (const matn of matnlar) {
  console.log(xatoKodi(matn));
}
console.log("admin:", {}.admin);
bash
node tekshiruv/paket.js
text
o'qildi
VazifaXatosi JSON: Matn JSON emas. Eksportni to'liq oling.
VazifaXatosi VERSIYA: Versiya 2 o'qilmaydi — faqat 1.
VazifaXatosi TUZILMA: Bu matn vazifalar eksporti emas.
VazifaXatosi VAZIFA: 1-vazifa: bajarildi true yoki false emas.
VazifaXatosi VAZIFA: 1-vazifa: id musbat, takrorlanmas son emas.
o'qildi
admin: undefined

Oxirgi ikki qator — himoya: __proto__ li paket o'qildi, lekin {}.admin — undefined. Hech bir obyektning prototipi o'zgarmadi.

node assets/js/render.js ni ham sinab ko'ring — ReferenceError: document is not defined. Bu kutilgan: render.js — brauzer qatlami. Mantiq ekrandan ajralgani uchun uni alohida tekshira oldik. Birinchi avtomatik test darsida aynan shu fayllar node:test ga o'tadi.

bash
git add tekshiruv/
git commit -m "Vazifalar: tekshiruvni paket va teglarga kengaytir"

11. 8-bosqich: PR va Pages

Branch'da shunday commitlar to'planishi kerak (sohalar — 09-qismdagi kelishuv):

# Commit xabari
1 Docs: oltin ssenariyni yoz
2 Vazifalar: boshlang'ich ma'lumotni tekshiruvga eksport qil
3 Vazifalar: importdagi ortiqcha himoyani olib tashla
4 Vazifalar: tekshiruvni paket va teglarga kengaytir
5 Docs: README ni v2 ga yangila

README.md ning yangi matni — «To'liq fayllar» ichida. Uni yozib, git add README.md va git commit -m "Docs: README ni v2 ga yangila" qiling. Keyin GitHub'ga yuboring va PR oching (GitHub CLI):

bash
git push -u origin feature/v2
gh pr create --fill
gh pr view --web

--fill sarlavha va tavsifni commitlardan oladi. Brauzerda Files changed yorlig'ini o'zingiz ko'rib chiqing — begona ko'z bo'lmasa, ertangi o'zingiz uchun. console.log qolmaganmi? // DEBUG yo'qmi? Keyin:

bash
gh pr merge --merge --delete-branch
git switch main
git pull

--merge — commitlar tarixi saqlansin: beshta kichik commit keyin git log da alohida o'qiladi (Merge). Pages "Deploy from a branch" rejimida — main yangilanishi bilan bir-ikki daqiqada sayt yangilanadi. https://login.github.io/vazifalar/ ni oching:

  • Console — toza.
  • Network (JS filtri, "Disable cache") — oltita .js, hammasi 200, modulepreload tufayli deyarli bir vaqtda.
  • Oltin ssenariy — 14 qadam, telefonda ham.

Biror modul 404 bersa — ikki tuzoqni tekshiring: / bilan boshlangan yo'l va katta-kichik harf (Brauzerda ESM).

To'liq fayllar: vazifalar.css va README.md

assets/css/vazifalar.css — 09-qismdagi fayl, oxiriga textarea uslubi qo'shilgan:

css
/* vazifalar.css — vazifalar ro'yxati v2 */
:root {
  --rang-matn: #1f2a24;
  --rang-fon: #f7faf8;
  --rang-asosiy: #1f6f4a;
  --rang-chegara: #c9d6cf;
  --rang-ikkinchi: #5b6660;
  --rang-xato: #b3261e;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font: 1rem/1.5 system-ui, sans-serif;
  color: var(--rang-matn);
  background: var(--rang-fon);
}
main { max-width: 36rem; margin: 0 auto; padding: 1rem; }
h1 { margin-top: 0; color: var(--rang-asosiy); }
h2 { margin-bottom: 0.25rem; font-size: 1.125rem; }
button, input, textarea { font: inherit; }
button {
  min-height: 2.75rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--rang-chegara);
  border-radius: 0.5rem;
  background: #fff;
  color: var(--rang-matn);
}
button[aria-pressed="true"] {
  border-color: var(--rang-asosiy);
  background: var(--rang-asosiy);
  color: #fff;
}
:focus-visible {
  outline: 3px solid var(--rang-asosiy);
  outline-offset: 2px;
}
.qator { display: flex; gap: 0.5rem; }
#yangi-matn, #tahrir-maydon {
  flex: 1;
  min-width: 0;
  min-height: 2.75rem;
  padding: 0.5rem;
  border: 2px solid var(--rang-ikkinchi);
  border-radius: 0.5rem;
}
[aria-invalid="true"] { border-color: var(--rang-xato); }
.xato {
  min-height: 1.5em;
  margin: 0.25rem 0 0;
  color: var(--rang-xato);
}
#filtrlar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
#royxat { margin: 0; padding: 0; list-style: none; }
#royxat li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--rang-chegara);
}
#royxat label { flex: 1; min-width: 8rem; overflow-wrap: anywhere; }
#royxat [type="checkbox"] {
  width: 1.25rem;
  height: 1.25rem;
}
.bajarildi label {
  color: var(--rang-ikkinchi);
  text-decoration: line-through;
}
#hisob { font-weight: bold; }
textarea {
  display: block;
  width: 100%;
  margin-block: 0.25rem 0.5rem;
  padding: 0.5rem;
  border: 2px solid var(--rang-ikkinchi);
  border-radius: 0.5rem;
}

README.md:

markdown
# Vazifalar

Vanilla JavaScript'da yozilgan vazifalar ro'yxati (v2).
Qo'shish, belgilash, tahrirlash, o'chirish, filtr va saralash;
statistika, teglar, JSON eksport va import. Klaviatura va ekran
o'quvchi bilan ishlaydi.

Sayt: https://login.github.io/vazifalar/

## Modullar

Kirish nuqtasi — `assets/js/asosiy.js` (`<script type="module">`).
Import faqat pastga qaraydi: asosiy → render → holat → royxat →
vazifa; paket → vazifa. Aylana import yo'q.

- `vazifa.js` — `Vazifa`, `VazifaXatosi`: bitta vazifa.
- `royxat.js` — `VazifalarRoyxati`: to'plam, hisob, teglar, obuna.
- `paket.js` — eksport va import: `{ versiya: 1, vazifalar }`.
- `holat.js` — `holat`, so'rovlar, o'zgartiruvchilar.
- `render.js` — DOM: chizish, fokus, e'lon.
- `asosiy.js` — tinglovchilar va ishga tushirish.

Yangi modul qo'shilsa — `index.html` dagi `modulepreload`
ro'yxatini ham yangilang.

## Kod qoidalari

- Holat — yagona haqiqat manbai; ekranni faqat `render()` chizadi.
- Royxat faqat o'z metodlari bilan o'zgaradi va `obuna` orqali
  xabar beradi.
- Tinglovchi — arrow funksiya, parametri `event`; `this` yo'q.
- Foydalanuvchi matni — faqat `textContent`.

## Tekshirish

- Mantiq (Node 24): `node tekshiruv/royxat.js` va
  `node tekshiruv/paket.js` — hamma qator ✅.
- Brauzer: `tekshiruv/ssenariy.md` dagi 14 qadam.

v2 ma'lumotni saqlamaydi — sahifa yangilansa, ro'yxat boshlang'ich
holatiga qaytadi. Saqlash — 11-qismda.

12. Baholash ro'yxati

O'zingiz baholang, yaxshisi — kursdoshingiz.

Majburiy shartlar. Bittasi bajarilmasa, loyiha qabul qilinmaydi:

  • Sayt https://login.github.io/vazifalar/ da ochiladi, Console'da qizil xato yo'q.
  • Oltin ssenariyning 14 qadami o'tadi.
  • node tekshiruv/royxat.js va node tekshiruv/paket.js — yo'q.
  • XSS sinovi: <img src=x onerror="console.log('XSS')"> vazifa matni bo'lib chiqadi.

Ballar (jami 100, o'tish — 80):

Bo'lim Nima tekshiriladi Ball
Modullar Olti fayl, ./ bilan nisbiy importlar, aylana yo'q, global'da nom yo'q 15
Vazifa #matn, getter, static, toJSON; VazifaXatosi da kod 10
VazifalarRoyxati Private maydonlar, closure'dagi id, iterator, obuna 15
HOF va hisob filter/toSorted/map, statistika() — yagona manba 10
Teglar Map sanog'i, Set bilan takrorsiz, difference 5
Eksport va import Format, oq ro'yxat, VazifaXatosi, "hammasi yoki hech narsa" 15
Xulq va qulaylik Fokus, e'lonlar, aria-pressed, aria-live — v1 dagidek 15
Node tekshiruvi Ikki fayl, hamma qator 5
Git Kichik commitlar, PR, README 10

Har bo'limda: hammasi bajarilgan — to'liq ball, bitta kamchilik — yarmi, ko'p — 0. 90–100 — portfolio kartasini "v2" ga yangilang. 80–89 — kamchiliklarni tuzating. 80 dan kam — ball yo'qolgan bo'limning tayanch darsiga qayting.

13. Ko'p uchraydigan xatolar

13.1 Import yo'li ./ siz

text
Uncaught TypeError: Failed to resolve module specifier "vazifa.js". Relative references must start with either "/", "./", or "../".

Tarjimasi: "vazifa.js modul nomini aniqlab bo'lmadi. Nisbiy yo'l /, ./ yoki ../ bilan boshlanishi kerak." "vazifa.js" — yalang nom (ESM import): brauzer uni fayl emas, paket nomi deb tushunadi va import xaritasidan qidiradi. Tuzatish: from "./vazifa.js".

13.2 Eksport unutilgan

text
Uncaught SyntaxError: The requested module './render.js' does not provide an export named 'boshMatnlar'

Tarjimasi: "So'ralgan ./render.js modulida boshMatnlar nomli eksport yo'q." Bu — SyntaxError: modul grafigi kod ishlashidan oldin bog'lanadi, shuning uchun bitta ham qator bajarilmaydi. Tuzatish: render.js da export const boshMatnlar.

13.3 Importdan keyin ekran "qotadi"

Import ishladi, lekin keyingi belgilash ekranni yangilamaydi, xato ham yo'q. Sabab: yangi royxatga obuna(render) qilinmagan. Tuzatish: royxatniAlmashtir dagi qayta obuna.

13.4 JSON.stringify(holat.vazifalar) — "{}"

Royxatning toJSON i yo'q: private maydonlar JSON'ga tushmaydi (class ichidan). Eksport bo'sh chiqadi. Tuzatish: toJSON() { return this.#vazifalar.map((v) => v.toJSON()); }.

13.5 Aylana import

holat.js ga import { render } from "./render.js" qo'shilsa va render.js yuqori darajada holat ni o'qisa — ReferenceError: Cannot access 'holat' before initialization. Tuzatish: render kerak bo'lgan funksiya asosiy.js da turadi.

14. Hujumchi nigohi

Import maydoniga foydalanuvchi — yoki unga "tayyor ro'yxat" yuborgan begona odam — istalgan matnni qo'yadi:

  • __proto__ va constructor kalitlari. Prototipni buzishga urinish (Prototype pollution). Bizda birlashtirish yo'q, vazifadan faqat oq ro'yxatdagi uch kalit Object.hasOwn bilan olinadi.
  • HTML va skript matnda. "matn": "<img src=x onerror=...>" — vazifa matni faqat textContent bilan chiziladi, kod bo'lib ishlamaydi (Kontentni o'zgartirish va XSS).
  • Takror va noto'g'ri id. Ikki vazifaga bitta id — top(id) ikkinchisini hech qachon topmaydi. malumotlarniTekshir uni rad etadi.
  • Uzun matn. HTML'dagi maxlength ni DevTools'da o'chirish mumkin. Vazifa.MAKS_UZUNLIK esa importda ham, tahrirda ham tekshiriladi.
  • Yarim import. Uchinchi vazifa buzuq bo'lsa, birinchi ikkitasi ham kirmaydi — eski ro'yxat joyida qoladi.

15. Mashqlar

1-mashq (oson): "Hamma vazifa bajarildi!"

Hamma vazifa bajarilganda hisobda "0 ta vazifa qoldi, 3 ta bajarildi" o'rniga Hamma vazifa bajarildi! chiqsin. Ro'yxat bo'sh bo'lsa — eski matn ("0 ta vazifa qoldi, 0 ta bajarildi"). Qaysi modul o'zgaradi?

Ishora: hisob matnini faqat render() yozadi; sonlar — statistika() dan.

Yechim

Faqat render.js — render() dagi hisob qatori:

js
const { qoldi, bajarildi } = holat.vazifalar.statistika();
const matn = qoldi === 0 && bajarildi > 0
  ? "Hamma vazifa bajarildi!"
  : `${qoldi} ta vazifa qoldi, ${bajarildi} ta bajarildi`;
matnniYoz(hisob, matn);

Boshqa hech bir fayl o'zgarmadi: hisob — hosila ma'lumot, uni bitta joy chizadi. Arxitekturaning foydasi shu — o'zgarish qayerda bo'lishini fayl nomidan bilasiz.

2-mashq (o'rta): brauzer yorlig'ida son

Brauzer yorlig'ida faol vazifalar soni ko'rinsin: (2) Vazifalar. Faol vazifa yo'q bo'lsa — Vazifalar. Uni render ga emas, royxatning ikkinchi obunachisi qilib yozing. Importdan keyin ham ishlashini tekshiring.

Ishora: document.title — yorliq matni. «Importdan keyin ekran qotadi» xatosini eslang.

Yechim

asosiy.js da:

js
function sarlavhaniYangila() {
  const { qoldi } = holat.vazifalar.statistika();
  document.title = qoldi > 0 ? `(${qoldi}) Vazifalar` : "Vazifalar";
}

function royxatniUla(royxat) {
  holat.vazifalar = royxat;
  royxat.obuna(render);
  royxat.obuna(sarlavhaniYangila);
  sarlavhaniYangila();
}

royxatniAlmashtir ning birinchi ikki qatori o'rniga — royxatniUla(VazifalarRoyxati.dan(malumotlar));. Fayl oxirida holat.vazifalar.obuna(render); o'rniga — royxatniUla(holat.vazifalar);.

Obuna bitta funksiyaga yig'ildi: ishga tushishda ham, importda ham bir xil yo'l. Oxirgi qator — sarlavhaniYangila() — ham kerak: obunachi faqat keyingi o'zgarishda chaqiriladi. Usiz importdan keyin yorliq eski sonda qolardi — biz buni Chrome'da ko'rdik.

3-mashq (qiyin): bajarilganlarni tozalash

Hisob ostiga <button type="button" id="tozala" hidden>Bajarilganlarni tozalash</button> qo'shing. Bosilganda bajarilgan hamma vazifa o'chsin, #elon da masalan "2 ta bajarilgan vazifa o'chirildi.", fokus "Ro'yxat" sarlavhasiga o'tsin. Bajarilgan vazifa yo'q bo'lsa — tugma yashirin.

Talab: o'chirish — VazifalarRoyxati ning yangi bajarilganlarniTozala() metodi; u o'chirilganlar sonini qaytarsin va obunachilarga bir marta xabar bersin. tekshiruv/royxat.js ga sinov qatori qo'shing.

Yechim

royxat.js — ochir dan keyin:

js
bajarilganlarniTozala() {
  const oldin = this.#vazifalar.length;
  this.#vazifalar = this.#vazifalar.filter((v) => !v.bajarildi);
  this.#xabarBer();
  return oldin - this.#vazifalar.length;
}

render.js: sarlavha oldiga export, yangi element — export const tozalaTugma = document.getElementById("tozala");, render() oxiriga — tozalaTugma.hidden = bajarildi === 0;.

asosiy.js (importga sarlavha va tozalaTugma qo'shiladi):

js
tozalaTugma.addEventListener("click", () => {
  holat.tahrirId = null;
  const soni = holat.vazifalar.bajarilganlarniTozala();
  sarlavha.focus();
  elonQil(`${soni} ta bajarilgan vazifa o'chirildi.`);
});

tekshiruv/royxat.js oxiriga — o'sha paytda 1- va 2-vazifa bajarilgan:

js
tekshir("tozalash", royxat.bajarilganlarniTozala(), 2);
tekshir("qolganlar", royxat.statistika().bajarildi, 0);

Ikkala qator ham . Bitta #xabarBer — bitta render: forEach ichida ochir() ni chaqirsangiz, har vazifa uchun ekran qayta chizilardi. sarlavha.focus() siz fokus yashiringan tugma bilan birga yo'qolardi (Dinamik UI'da qulaylik).

16. Real ishda

  • Refaktor — kundalik ish. Ko'p vaqt yangi kod yozishga emas, mavjudini o'qish va qayta tuzishga ketadi. "Oltin ssenariy", kichik commitlar, PR'da o'z kodini ko'rib chiqish — har jamoada shunday.
  • Qatlamlar. Ma'lumot va qoidalar (Vazifa, VazifalarRoyxati), almashinuv (paket.js), ko'rinish (render.js) — React ilovalarida ham, backend'da ham aynan shu bo'linish uchraydi. 17-qismda render.js React komponentlariga almashadi, royxat.js va paket.js mantig'i esa saqlanib qoladi.
  • Intervyu. "Kodni qanday modullarga bo'lasiz?", "Kuzatuvchi naqshini qayerda ishlatgansiz?", "JSON importni qanday xavfsiz qilasiz?" — bu loyiha uchala savolga tayyor javob.

10-qism yakuni

Tabriklaymiz — "JavaScript chuqur" qismi tugadi! Qism boshida for...of bilan qo'lda yozgan korinadiganlar endi bitta metod zanjiri. Bitta faylli skript esa oltita mustaqil modulga aylandi. Bu qismda:

  • Massiv metodlari: map, filter, reduce, toSorted, flatMap, Object.groupBy — ma'lumot pipeline'i.
  • Obyektlar: getter, deskriptor, freeze, immutable yangilash, JSON va uning chegaralari.
  • Ichki mexanizmlar: call stack, scope, hoisting, TDZ, closure, this, strict mode, prototiplar.
  • Klasslar: private maydon, static, meros va kompozitsiya, dizayn naqshlari.
  • To'plamlar va iteratsiya: Map, Set, Symbol, iterator, generator.
  • Xatolar va modullar: try/catch, o'z xato klassi, using, ESM va CommonJS.

Oldinda nima bor

v2 ning ikki kamchiligi bor. Birinchisi — u ma'lumotni saqlamaydi: sahifa yangilansa, vazifalar yo'qoladi. Ikkinchisi — uning hamma ishi bir zumda bajariladi, u hech narsani kutmaydi. Serverdan ma'lumot olish yoki faylni o'qish esa vaqt oladi — kutish paytida sahifa qotmasligi kerak.

11-qism — "Asinxron JavaScript va tarmoq" — aynan shu haqida. Taymerlar, event loop, Promise, async/await va fetch ni o'rganasiz. vazifalar brauzer xotirasida saqlanadigan bo'ladi (localStorage). Bugungi toJSON, VazifalarRoyxati.dan va paketniOqi o'sha yerda deyarli o'zgarmay ishlaydi — saqlash ham, yuklash ham shu format orqali o'tadi.

Xulosa

  • Refaktorning oltin qoidasi — xulq o'zgarmaydi; buni oldindan yozilgan ssenariy va Node tekshiruvi kafolatlaydi.
  • Modullar pastdan yuqoriga quriladi, importlar faqat pastga qaraydi — aylana yo'q.
  • VazifalarRoyxati — private ma'lumot, closure'dagi id, statistika(), teglar, iterator va kuzatuvchi bitta seyfda.
  • Bir ish — bir joy: hisob statistika() da, DOM render.js da, JSON paket.js da.
  • Import — chegara: oq ro'yxat, VazifaXatosi, fail fast va "hammasi yoki hech narsa".
  • DOM'siz mantiqni Node'da brauzersiz tekshirish mumkin — bu 12-qismdagi avtomatik testlarning poydevori.

Keyingi dars: Sinxron va asinxron kod — 11-qism boshlanadi: JavaScript nega bitta oqimda ishlaydi va uzoq kutish sahifani qanday qotirib qo'yadi.

Manbalar

  • MDN: "JavaScript modules", "Private properties", "Object.groupBy()", "Set.prototype.difference()" — developer.mozilla.org
  • MDN: "JSON.stringify()" (toJSON, replacer massivi), "Error: cause" — developer.mozilla.org
  • Martin Fowler: "Refactoring" (2-nashr) — refactoring.com
  • GitHub Docs: "Creating a pull request", "Configuring a publishing source for your GitHub Pages site" — docs.github.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript yakuniy loyiha: vazifalar v2 — modullar, klasslar va closure — IlmHamroh