Mundarija (42)
- Bu darsda
- 1. Nega bu kerak?
- 2. Loyiha talablari
- 2.1 Ilova nima qiladi
- 2.2 Refaktorning oltin qoidasi
- 2.3 Maqsadli arxitektura
- 3. Reja: sakkiz bosqich
- 4. 1-bosqich: branch va oltin ssenariy
- 5. 2-bosqich: vazifa.js
- 6. 3-bosqich: royxat.js
- 6.1 Seyf: private maydonlar, closure va kuzatuvchi
- 6.2 So'rovlar: HOF, guruhlash, Map va Set
- 7. 4-bosqich: holat.js
- 8. 5-bosqich: paket.js
- 8.1 Maydonlar ro'yxati — bitta joyda
- 8.2 vazifaniOl — standart qiymatsiz
- 8.3 xavfsizBirlashtir olib tashlanadi
- 8.4 Tayyor fayl
- 9. 6-bosqich: render.js, asosiy.js va index.html
- 9.1 render.js — ekran qatlami
- 9.2 asosiy.js — kirish nuqtasi
- 9.3 index.html
- 9.4 Hammasi bitta oynada — jonli sinov
- 10. 7-bosqich: Node tekshiruvi
- 11. 8-bosqich: PR va Pages
- 12. Baholash ro'yxati
- 13. Ko'p uchraydigan xatolar
- 13.1 Import yo'li ./ siz
- 13.2 Eksport unutilgan
- 13.3 Importdan keyin ekran "qotadi"
- 13.4 JSON.stringify(holat.vazifalar) — "{}"
- 13.5 Aylana import
- 14. Hujumchi nigohi
- 15. Mashqlar
- 1-mashq (oson): "Hamma vazifa bajarildi!"
- 2-mashq (o'rta): brauzer yorlig'ida son
- 3-mashq (qiyin): bajarilganlarni tozalash
- 16. Real ishda
- 10-qism yakuni
- Oldinda nima bor
- Xulosa
- Manbalar
JavaScript yakuniy loyiha: vazifalar v2 — modullar, klasslar va closure
Qisqacha: Bu darsda 10-qism davomida qadamma-qadam o'zgargan
vazifalarilovasini bitta toza arxitekturaga yig'amiz. Ilova oltita ES modulga bo'linadi:VazifavaVazifalarRoyxatiklasslari, closure bilan id, HOF bilan ro'yxat va hisob,Map/Setbilan 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
xavfsizBirlashtirvavazifaniOldagi standart qiymatlar. - Mantiq qatlamini brauzersiz — Node 24 da — tekshirasiz.
vazifalarv2 ni PR orqalimainga 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.jsda Prototype pollution darsidagixavfsizBirlashtirhamon turibdi,vazifaniOlesa yetishmagan maydonga standart qiymat qo'yadi — Xato strategiyasi dagi tekshiruvdan keyin ikkalasi ham ortiqcha; - tekshiruv:
tekshiruv/royxat.jsfaqat 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:
- 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). - Saralash. Bajarilmaganlar tepada, guruh ichida — qo'shilgan tartib (
sort). - Statistika. "2 ta vazifa qoldi, 1 ta bajarildi" (Guruhlash).
- Teglar. "Teglar: #bozor — 2, #uy — 1. Yopilgan: #uy" (
Setto'plam metodlari). - 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 ulaydiganasosiy.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:
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.mdHar 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 --> VDiagrammaga 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
cd ~/kurs/vazifalar
git switch main
git pull
git switch -c feature/v2Branch 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.
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:
// 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—kodbilan. Importkodga tayanadi ("JSON","VERSIYA","VAZIFA"). Faylingizdakodyo'q bo'lsa — O'z xato klasslaringiz darsidagidek qo'shing.Vazifa— o'zgarishsiz.#matnprivate, o'qish getter orqali, o'zgartirishmatnniOzgartirorqali. U xato tashlamaydi,true/falseqaytaradi: tahrirda noto'g'ri matn — oddiy holat, istisno emas.toJSONvastatic 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
importyo'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.
// 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 — faqatthis.#yangiId()chaqiriladi.engKatta + 1. Closure amaliyotda darsida boshlang'ich4qo'lda yozilgan edi, "11-qismda yuklangan ro'yxatdan hisoblanadi" degan va'da bilan. Va'da bajarildi:danistalgan 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). obunava#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]vafor...ofishlaydi, 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 darsidagiqoldio'z hisobini yuritmaydi —statistika().qoldini qaytaradi. Ikki joyda sanash — ikki xil javob xavfi.Object.groupBybo'sh guruhni yaratmaydi, shuning uchunguruhlar.faol?.length ??dan keyin turadi.toSorted—#vazifalartartibi hech qachon o'zgarmaydi, ekran uchun saralangan nusxa olinadi (Nusxa bilan o'zgartirish).- Teglar royxat orqali.
tegHisobi(Map) vayopilganTeglar(Setto'plam metodlari) vazifalar to'plami haqidagi savol — joyi ham to'plamda. Ular globalholatni emas,this.#vazifalarni o'qiydi. ochir—filterbilan.findIndex+spliceikki qadam va bitta tuzoq bo'lardi: id topilmasafindIndex-1beradi,splice(-1, 1)esa oxirgi vazifani o'chiradi.filterbitta qatorda yangi massiv yasaydi — royxatning qolgan metodlari kabi.pushklass ichida — xavfsiz. Immutable yangilash qoidasi oddiy ma'lumot uchun edi.#vazifalarni esa tashqi kod umuman ko'rmaydi. Tashqariga doim yangi massiv chiqadi:korinadiganlarham,toJSONham.
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:
// 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
// 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.danulardan yangiVazifanusxalarini 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 uchunVazifani 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).
vazifaQoshda avval filtr,matnniAlmashtirda avvaltahrirId, oxirida royxat. Royxat o'zgarishi bilanrenderishlaydi — unga kerak bo'lgan holat tayyor bo'lishi shart.
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
// oldin
const XAVFLI_KALITLAR = ["__proto__", "constructor", "prototype"];
const VAZIFA_MAYDONLARI = ["id", "matn", "bajarildi"];
const eksportMaydonlari = [
"versiya", "vazifalar", "id", "matn", "bajarildi",
];// 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
// oldin
function vazifaniOl(xom) {
const vazifa = { id: 0, matn: "", bajarildi: false };
for (const maydon of VAZIFA_MAYDONLARI) {// 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:
bajarildisiz vazifa endi "bajarildi true yoki false emas." bilan rad etiladi. Avval u jimginafalseolardi."vazifalar": [null]endi "1-vazifa: id …" xabarini oladi. AvvalObject.hasOwn(null, …)TypeErrorberardi 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
// oldin — paketniOqi ichida
const paket = xavfsizBirlashtir({}, xom);
if (paket.versiya !== 1) {
// ...
const malumotlar = paket.vazifalar.map(vazifaniOl);// 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:
vazifaniOldagi 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
// 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.
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 —teglarniYozbilan. Ikkalasi ham royxatdan so'raydi, o'zi sanamaydi. boshMatnlareksport qilinadi. U uchta to'g'ri filtr nomining yagona ro'yxati —filtrniTanlashundan foydalanadi.- DOM bilan ishlaydigan hamma narsa shu yerda: eksport va import elementlari ham,
importXatosiniKorsatham.asosiy.jsdocument.getElementByIdni deyarli chaqirmaydi.
To'liq fayl: assets/js/render.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.danyangi obyekt yasaydi, eski royxatning obunachilari unga ko'chmaydi.obuna(render)qatorini unutsangiz, import ishlaydi, lekin keyingi bosishlar ekranni yangilamaydi — jim xato.- Nega
royxatniAlmashtirholat.jsda emas? Ungarenderkerak.holat.jsrender.jsni import qilsa, aylana paydo bo'ladi — Modul semantikasi darsidagi TDZ tuzog'i.filtrniTanlaham shu sababliasosiy.jsda. filtrniTanla—Object.hasOwnbilan (Object.*metodlari)."constructor"yoki"toString"—boshMatnlarning meros kaliti, filtr emas (Prototip zanjiri).- Import tinglovchisi
instanceof VazifaXatosibilan o'z xatosini ajratadi. Begona xato — bug: foydalanuvchiga umumiy xabar, Console'ga esa qayta tashlanadi.
To'liq fayl: assets/js/asosiy.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)nivazifaniAlmashtir(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
<!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:
<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:
HTMLCollection(3) [li, li, li.bajarildi]
2 ta vazifa qoldi, 1 ta bajarildi
3li.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:
{
"name": "vazifalar",
"private": true,
"type": "module"
}tekshiruv/royxat.js — CommonJS va ESM darsidagi faylni kengaytiramiz: endi royxat, holat va paketning asosiy va'dalari:
// 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");node tekshiruv/royxat.js✅ 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 damlashUch 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:
// 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);node tekshiruv/paket.jso'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: undefinedOxirgi 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.
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):
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:
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, hammasi200,modulepreloadtufayli 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:
/* 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:
# 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.jsvanode 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
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
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__vaconstructorkalitlari. Prototipni buzishga urinish (Prototype pollution). Bizda birlashtirish yo'q, vazifadan faqat oq ro'yxatdagi uch kalitObject.hasOwnbilan olinadi.- HTML va skript matnda.
"matn": "<img src=x onerror=...>"— vazifa matni faqattextContentbilan 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.malumotlarniTekshiruni rad etadi. - Uzun matn. HTML'dagi
maxlengthni DevTools'da o'chirish mumkin.Vazifa.MAKS_UZUNLIKesa 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:
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:
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:
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):
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:
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-qismdarender.jsReact komponentlariga almashadi,royxat.jsvapaket.jsmantig'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, DOMrender.jsda, JSONpaket.jsda. - 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!