IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging8/29-dars20 daqiqa
Mundarija (33)

DOM bo'ylab harakatlanish: parentElement, children, nextElementSibling va closest

Qisqacha: Bitta elementni topgach, uning "qarindoshlari"ga qidiruvsiz o'tish mumkin: otasi — parentElement, bolalari — children, birinchi va oxirgi bolasi — firstElementChild va lastElementChild, qo'shnilari — previousElementSibling va nextElementSibling. Har birining "Element" so'zisiz egizagi ham bor, lekin u bo'sh joy matn tugunlarini ham qaytaradi. Uzoqdagi otani esa closest bilan topgan ma'qul.

Bu darsda

  • Elementdan otasiga, bolalariga va qo'shnilariga o'ta olasiz.
  • Tugun va element xususiyatlari farqini bilib, bo'sh joy tuzog'idan qochasiz.
  • Qo'shnilar bo'ylab sikl yozib, ro'yxat oxirini null bilan aniqlaysiz.
  • Qachon parentElement zanjiri, qachon closest yaxshi ekanini tushuntira olasiz.
  • Butun daraxtni rekursiya bilan aylanib chiqa olasiz.

Oldin bilishingiz kerak: DOM daraxti va tugun turlari, Elementlarni tanlash, Element yaratish, joylash va o'chirish, while sikli.

1. Nega bu kerak?

Ofitsiant Otabek «Bahor» zalida turibdi. Jasur aka unga: "Manti olgan stolning yonidagi stolga choy olib bor", deydi. Otabek butun zalni qaytadan aylanmaydi. U manti stolini biladi, yonidagi stol esa shundoqqina qo'l uzatsa yetadigan joyda.

JavaScript'da ham shunday vaziyatlar ko'p. Elementlarni tanlash darsida elementni selektor bilan topishni o'rgandik. Lekin ko'pincha bitta element allaqachon qo'lingizda bo'ladi, sizga esa uning yonidagisi kerak:

  • Savol tugmasi bosildi — undan keyingi javob blokini ochish kerak.
  • Menyuda joriy sahifa havolasi bor — uning li sini va qo'shni sahifalarni bilish kerak.
  • Ro'yxatda "tanlangan" taom bor — "Keyingi" tugmasi tanlovni pastdagi taomga surishi kerak.

Har safar butun sahifadan qidirish o'rniga, DOM daraxtida bir qadam yuqoriga, pastga yoki yonga yuramiz. Bu darsda shu qadamlarning hammasini ko'ramiz.

2. Daraxt bo'ylab olti yo'nalish

2.1 Oila shajarasi

DOM daraxti darsida tugunlar oila so'zlari bilan atalishini ko'rgandik: ota, bola, aka-uka (qo'shni). Endi har munosabatga JavaScript'da qanday "yetib borish"ni ko'ramiz.

Diagrammada «Bahor» menyusi va o'rtadagi Manti qatori. Tutash strelkalar — ul dan pastga, uzuq strelkalar — Manti'dan qarindoshlariga:

flowchart TD
  UL["ul#menyu"]
  OSH["li: Osh"]
  MANTI["li: Manti"]
  LAG["li: Lag'mon"]
  UL -->|"firstElementChild"| OSH
  UL -->|"children[1]"| MANTI
  UL -->|"lastElementChild"| LAG
  MANTI -.->|"parentElement"| UL
  MANTI -.->|"previousElementSibling"| OSH
  MANTI -.->|"nextElementSibling"| LAG

Hamma nom inglizcha so'zlardan yasalgan va o'zi tarjima qilinadi: parent — ota, child — bola, sibling — aka-uka (qo'shni), previous — oldingi, next — keyingi, first va last — birinchi va oxirgi.

2.2 Ikki to'plam: tugun va element

Har yo'nalish uchun ikkita xususiyat bor. Biri har qanday tugunni qaytaradi — matn va izohni ham. Ikkinchisi nomida Element so'zi bor va faqat elementlarni qaytaradi:

Yo'nalish Har qanday tugun Faqat element
Ota parentNode parentElement
Bolalar childNodes children
Birinchi bola firstChild firstElementChild
Oxirgi bola lastChild lastElementChild
Oldingi qo'shni previousSibling previousElementSibling
Keyingi qo'shni nextSibling nextElementSibling

Nega ikkita? Tarix sababli. Chap ustundagilar DOM'ning birinchi versiyalaridan beri bor. O'ng ustundagilar keyinroq qo'shildi — aynan bo'sh joy matn tugunlari hammani charchatgani uchun.

Qoida oddiy: sizga deyarli doim o'ng ustun kerak. Chap ustun — matn tugunlari bilan ataylab ishlaganda yoki "nega sanoq to'g'ri chiqmayapti?" degan savolga javob izlaganda.

Tekshirib ko'ring: <p>Narxi: <b>35 000</b> so'm</p> da p.firstChild va p.firstElementChild nima?

Javob

p.firstChild — "Narxi: " matn tuguni, chunki p ning birinchi bolasi aynan shu matn. p.firstElementChild — <b>35 000</b>, chunki matnni tashlab, birinchi element bolani qaytaradi.

3. Pastga: bolalarga o'tish

3.1 Birinchi, oxirgi va N-chi bola

DOM daraxti darsida va'da qilgan edik: firstChild ning bo'sh joy tuzog'ini firstElementChild yechadi. Mana u:

html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
  <li>Lag'mon</li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  console.log(menyu.firstChild.nodeName, menyu.firstElementChild);
  console.log(menyu.lastChild.nodeName, menyu.lastElementChild);
  console.log(menyu.children[1], menyu.childElementCount);
</script>

Konsolda:

text
#text <li>Osh</li>
#text <li>Lag'mon</li>
<li>Manti</li> 3

Qatorma-qator:

  • firstChild — <ul> dan keyingi Enter va bo'sh joylar, ya'ni #text. firstElementChild esa birinchi li ni to'g'ri topdi.
  • lastChild — </li> dan keyingi Enter, yana #text. lastElementChild — Lag'mon.
  • children[1] — ikkinchi element bola (sanoq noldan). childElementCount — nechta element bola bor. U children.length bilan bir xil.

Bola bo'lmasa nima bo'ladi? firstElementChild va lastElementChild null qaytaradi, children esa bo'sh to'plam. children[10] — undefined, xuddi massivdagidek.

3.2 children bo'ylab aylanish

children — HTMLCollection, uni for...of bilan aylanib chiqish mumkin:

html
<ul id="menyu">
  <li>Osh — 35 000</li>
  <li>Manti — 30 000</li>
  <li>Lag'mon — 28 000</li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  for (const taom of menyu.children) {
    console.log(taom.textContent);
  }
  console.log(menyu.children);
</script>

Konsolda:

text
Osh — 35 000
Manti — 30 000
Lag'mon — 28 000
HTMLCollection(3) [li, li, li]

Eslatma: Elementlarni tanlash darsida aytilganidek, children — jonli to'plam. Sikl ichida bola qo'shsangiz yoki o'chirsangiz, to'plam ham o'zgaradi. Bunday holda avval statik nusxa oling: [...menyu.children] (spread).

children va querySelectorAll ning farqi ham muhim. menyu.children — faqat bevosita bolalar. menyu.querySelectorAll("li") — ichma-ich joylashgan hamma li, nevaralar ham. Ichki ro'yxati bor menyuda bu farq katta.

Tekshirib ko'ring: menyu.children va menyu.querySelectorAll(":scope > *") bir xil elementlarni beradimi? Farqi nimada?

Javob

Elementlari bir xil — ikkalasi ham bevosita element bolalar. Farqi to'plam turida: children — jonli HTMLCollection, querySelectorAll — statik NodeList. DOM o'zgarsa, birinchisi o'zi yangilanadi, ikkinchisi — yo'q.

4. Yonga: qo'shnilarga o'tish

4.1 Oldingi va keyingi

Bir otaning bolalari — qo'shnilar. Manti qatoridan ikki tomonga qaraymiz:

html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
  <li>Lag'mon</li>
</ul>
<script>
  const manti = document.getElementById("menyu").children[1];
  console.log(manti.previousElementSibling);
  console.log(manti.nextElementSibling);
  console.log(manti.nextElementSibling.nextElementSibling);
  console.log(manti.nextSibling.nodeName);
</script>

Konsolda:

text
<li>Osh</li>
<li>Lag'mon</li>
null
#text
  • Manti'dan oldin — Osh, keyin — Lag'mon.
  • Lag'mon'dan keyin hech kim yo'q — null. Bu "qatorning oxiri" degan signal.
  • nextSibling — yana Enter va bo'sh joy. Element kerak bo'lsa, "Element" so'zli versiyani oling.

Hayotdan o'xshatish: navbatda turgan odamlar. Har kim o'zidan oldingi va keyingi odamni biladi. Navbat boshidagi odamning "oldingisi" — hech kim, oxiridagining "keyingisi" — hech kim. JavaScript buni null deydi.

4.2 null gacha yurish

null signali qo'shnilar bo'ylab sikl yozishga imkon beradi. Birinchi elementdan boshlaymiz va null ga yetguncha keyingisiga o'tamiz (while sikli):

html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
  <li>Lag'mon</li>
  <li>Ko'k choy</li>
</ul>
<script>
  let taom = document.getElementById("menyu").firstElementChild;
  let raqam = 1;
  while (taom !== null) {
    console.log(`${raqam}. ${taom.textContent}`);
    taom = taom.nextElementSibling;
    raqam++;
  }
</script>

Konsolda:

text
1. Osh
2. Manti
3. Lag'mon
4. Ko'k choy

Bu yerda for...of va children ham xuddi shu natijani berardi. while ning kuchi boshqa joyda: siz istalgan elementdan boshlab, istalgan tomonga yura olasiz. Masalan, Manti'dan orqaga qarab nechta taom borligini sanash mumkin — uchinchi mashqda shuni qilasiz.

4.3 Amaliy holat: "Keyingi taklif" tugmasi

«Bahor» bosh sahifasida "Bugungi taklif" ro'yxati bor. Bitta taom ajratilgan. Tugma bosilganda ajratish keyingi taomga o'tishi kerak, oxirgisidan keyin esa yana birinchisiga.

Tugma bosilishiga javob berish uchun addEventListener kerak. Uni addEventListener asoslari darsida to'liq o'rganamiz. Hozircha bitta qolip yetadi: tugma.addEventListener("click", funksiya) — "tugma bosilganda shu funksiyani chaqir":

html
<style>
  .tanlangan { background: #e8f1ec; font-weight: bold; }
</style>
<ul id="takliflar">
  <li class="tanlangan">Osh</li>
  <li>Manti</li>
  <li>Lag'mon</li>
</ul>
<button type="button" id="keyingi">Keyingi taklif</button>
<script>
  const royxat = document.getElementById("takliflar");

  function keyingisigaOt() {
    const joriy = royxat.querySelector(".tanlangan");
    const keyingi =
      joriy.nextElementSibling ?? royxat.firstElementChild;
    joriy.classList.remove("tanlangan");
    keyingi.classList.add("tanlangan");
    console.log(`Taklif: ${keyingi.textContent}`);
  }

  console.log("Boshida: Osh");
  const tugma = document.getElementById("keyingi");
  tugma.addEventListener("click", keyingisigaOt);
</script>

Konsolda:

text
Boshida: Osh

Endi "Keyingi taklif" tugmasini uch marta bosing. Ajratish Manti'ga, keyin Lag'mon'ga o'tadi, konsolda Taklif: Manti va Taklif: Lag'mon chiqadi. Uchinchi bosishda nextElementSibling null beradi, ?? operatori esa uning o'rniga firstElementChild ni oladi — tanlov yana Osh'ga qaytadi. classList bilan class qo'shish va olib tashlash — classList va stillar darsidan tanish.

E'tibor bering: kod taomlar sonini bilmaydi. Ro'yxatga yana o'nta taom qo'shilsa ham, u o'zgarishsiz ishlaydi.

Tekshirib ko'ring: "Oldingi taklif" tugmasi ham kerak bo'lsa, keyingisigaOt dagi qaysi ikki so'zni almashtirib, yangi funksiya yasaysiz?

Javob

nextElementSibling o'rniga previousElementSibling, firstElementChild o'rniga lastElementChild. Birinchi taomdan orqaga qadam null beradi va tanlov ro'yxat oxiriga — Lag'mon'ga o'tadi. Qolgan kod o'zgarmaydi.

4.4 Amaliy holat: savol va javob

«Bahor» saytida "Ko'p beriladigan savollar" bo'limi bor. Har savol — tugma, javob esa uning ortidan keladigan va boshida yashirin paragraf. Tugma bosilganda javob ochiladi, yana bosilsa — yopiladi.

Har tugmaga alohida id berib, har javobni alohida topish shart emas. Tugma o'z javobini biladi: u keyingi qo'shni:

html
<h2>Ko'p beriladigan savollar</h2>
<button type="button">Ish vaqti qanday?</button>
<p hidden>Har kuni 7:00 dan 23:00 gacha.</p>
<button type="button">Stol band qilsa bo'ladimi?</button>
<p hidden>Ha, 10:00 dan 22:00 gacha qabul qilinadi.</p>
<script>
  function javobniAlmashtir(tugma) {
    const javob = tugma.nextElementSibling;
    javob.hidden = !javob.hidden;
    console.log(`${tugma.textContent} → ochiq: ${!javob.hidden}`);
  }

  for (const tugma of document.querySelectorAll("button")) {
    tugma.addEventListener("click", () => javobniAlmashtir(tugma));
  }
</script>

Ikkinchi savolni bosing: uning ostida javob paydo bo'ladi, konsolda Stol band qilsa bo'ladimi? → ochiq: true. Yana bossangiz — javob yashirinadi va ochiq: false chiqadi. Birinchi savolning javobi bu vaqtda o'zgarmaydi: har tugma faqat o'z qo'shnisiga tegadi.

Ikki yangi detal. hidden — elementni yashiradigan xususiyat, u true yoki false bo'ladi (Atributlar va xususiyatlar). !javob.hidden — teskarisi: yashirin bo'lsa ochadi, ochiq bo'lsa yashiradi. Sikl ichidagi () => javobniAlmashtir(tugma) — har tugma uchun o'zining tugmasini eslab qoladigan kichik arrow funksiya.

Bunday blokni real saytda HTML'ning o'zi ham yasay oladi — details va summary teglari (details va summary). Bu misol esa qo'shni orqali yurish g'oyasini ko'rsatish uchun. JavaScript kerak bo'ladigan murakkab akkordeonlarda (masalan, bittasi ochilsa boshqalari yopilsin) aynan shu nextElementSibling yondashuvi ishlatiladi. Malika kabi ekran o'quvchi foydalanuvchilar uchun tugmaga "ochiq/yopiq" holatini aytadigan atribut ham qo'shiladi — buni Dinamik UI'da qulaylik darsida ko'ramiz.

5. Yuqoriga: otaga o'tish

5.1 parentElement va parentNode

Otaga ham ikki yo'l bor. Deyarli har doim ular bir xil elementni qaytaradi. Farq faqat daraxtning eng tepasida chiqadi:

html
<p>Narxi: <span id="narx">30 000</span> so'm</p>
<script>
  const narx = document.getElementById("narx");
  console.log(narx.parentElement);
  console.log(narx.parentNode === narx.parentElement);

  const html = document.documentElement;
  console.log(html.parentNode.nodeName);
  console.log(html.parentElement);
</script>

Konsolda:

text
<p>…</p>
true
#document
null

<html> ning otasi — document. U tugun, lekin element emas. Shuning uchun parentNode uni qaytaradi, parentElement esa null beradi. Kundalik ishda parentElement ni ishlating — u doim yo element, yo null.

Konsoldagi <p>…</p> ga qarang: p ichida span bor, shuning uchun konsol ichini … bilan qisqartirdi (Brauzer muhiti darsidagi belgi).

5.2 Zanjir va uning xavfi

Otaning otasiga o'tish uchun parentElement ni zanjir qilib yozish mumkin:

html
<article class="taom-karta">
  <h2>Manti</h2>
  <p>Narxi: <span class="narx">30 000</span> so'm</p>
</article>
<script>
  const narx = document.querySelector(".narx");
  const karta = narx.parentElement.parentElement;
  console.log(karta);
  console.log(karta === narx.closest(".taom-karta"));
</script>

Konsolda:

text
<article class="taom-karta">…</article>
true

Ikki yo'l bir xil kartani topdi. Lekin ertaga dizayner Nodira narxni qalin qilmoqchi bo'ldi va span ni <strong> ichiga o'radi:

html
<article class="taom-karta">
  <h2>Manti</h2>
  <p>Narxi: <strong><span class="narx">30 000</span></strong> so'm</p>
</article>
<script>
  const narx = document.querySelector(".narx");
  console.log(narx.parentElement.parentElement);
  console.log(narx.closest(".taom-karta"));
</script>

Konsolda:

text
<p>…</p>
<article class="taom-karta">…</article>

Zanjir endi kartaga emas, p ga yetdi — bir qavat kam. Xato xabari yo'q, kod shunchaki noto'g'ri elementni oladi. closest esa HTML qanday o'zgarmasin, eng yaqin .taom-karta ni topadi.

Qoida:

  • Bitta qadam, tuzilma aniq (havolaning li si, tugmaning bevosita otasi) — parentElement.
  • Uzoqdagi ota, ma'nosi bo'yicha ("bu narx qaysi kartaga tegishli?") — closest.

5.3 contains — "ichidami?"

Ba'zan yurish emas, savol kerak bo'ladi: "bu element anavi blokning ichidami?" Buning uchun contains bor. U true yoki false qaytaradi:

html
<ul id="menyu">
  <li id="osh">Osh</li>
</ul>
<p id="izoh">Narxlar so'mda</p>
<script>
  const menyu = document.getElementById("menyu");
  const osh = document.getElementById("osh");
  const izoh = document.getElementById("izoh");
  console.log(menyu.contains(osh), menyu.contains(izoh));
  console.log(osh.contains(menyu), menyu.contains(menyu));
</script>

Konsolda:

text
true false
false true

contains faqat bevosita bolani emas, istalgan chuqurlikdagi avlodni tekshiradi. Oxirgi qatorga qarang: element o'zini ham "ichida" deb hisoblaydi — closest o'zidan boshlagani kabi. contains hodisalar bilan ishlaganda kerak bo'ladi: masalan, "foydalanuvchi menyuning tashqarisini bosdimi?" degan savolga u javob beradi.

Tekshirib ko'ring: karta.contains(narx) va narx.closest(".taom-karta") === karta — har doim bir xil javob beradimi?

Javob

Yo'q. Kartalar ichma-ich joylashgan bo'lsa, farq chiqadi: tashqi karta ham ichki kartaning narxini "o'z ichida" deb biladi (contains — true). closest esa faqat eng yaqin kartani qaytaradi, ya'ni ichkisini. Oddiy, ichma-ich bo'lmagan kartalarda javob bir xil.

6. Butun daraxtni aylanib chiqish

Ba'zan bitta qadam emas, butun daraxt kerak bo'ladi: masalan, ichma-ich menyuning tuzilishini konsolda ko'rish. Ichki ro'yxat ichida yana ro'yxat bo'lishi mumkin — necha qavat ekanini oldindan bilmaymiz.

Bunday "ichida o'ziga o'xshash narsa" tuzilmasi uchun rekursiya yaratilgan. Funksiya elementni chiqaradi, keyin har bolasi uchun o'zini chaqiradi. To'xtash sharti (base case) alohida yozilmagan: bolasi yo'q elementda sikl bir marta ham aylanmaydi va funksiya o'zi tugaydi.

html
<ul id="menyu">
  <li>Taomlar
    <ul>
      <li>Osh</li>
      <li>Manti</li>
    </ul>
  </li>
  <li>Ichimliklar
    <ul>
      <li>Ko'k choy</li>
    </ul>
  </li>
</ul>
<script>
  function daraxtniChiqar(element, chuqurlik) {
    const nom = element.firstChild?.nodeValue?.trim() ?? "";
    const chekinish = "  ".repeat(chuqurlik);
    console.log(`${chekinish}${element.nodeName} ${nom}`);
    for (const bola of element.children) {
      daraxtniChiqar(bola, chuqurlik + 1);
    }
  }
  daraxtniChiqar(document.getElementById("menyu"), 0);
</script>

Konsolda:

text
UL
  LI Taomlar
    UL
      LI Osh
      LI Manti
  LI Ichimliklar
    UL
      LI Ko'k choy

Qatorma-qator:

  • element.firstChild?.nodeValue — birinchi bola tugunning matni. li uchun bu "Taomlar" yoki "Osh". ul uchun — Enter va bo'sh joylar, trim ularni bo'sh satrga aylantiradi (Matnni tozalash).
  • ?. va ?? — birinchi bola bo'lmasa yoki u element bo'lsa (nodeValue — null), xato o'rniga bo'sh satr olish uchun.
  • " ".repeat(chuqurlik) — har qavatga ikki bo'sh joy chekinish (Kesish va to'ldirish).
  • for...of element.children bo'ylab — har element bola uchun funksiya o'zini bir qavat chuqurroq chaqiradi.

Bu — daraxtni aylanish (tree traversal): har tugunga bir martadan kirib chiqish. Brauzerning o'zi ham querySelectorAll ni shunga o'xshash aylanish bilan bajaradi. Brauzerda bu ish uchun tayyor asboblar ham bor (TreeWalker), lekin ular kamdan-kam kerak bo'ladi va kundalik ishda yozilgan rekursiya yetadi.

Tekshirib ko'ring: Funksiyada element.children o'rniga element.childNodes yozilsa, konsolda nima o'zgaradi?

Javob

Har element orasida qo'shimcha #text qatorlari paydo bo'ladi — teglar orasidagi Enter va bo'sh joylar. Funksiya ularga ham kiradi: matn tugunining bolasi yo'q, shuning uchun u yerda rekursiya darhol tugaydi, lekin qatorlar 8 ta emas, 23 ta bo'ladi — Chrome'da sanadik. Tuzilmani ko'rish uchun children aniqroq.

7. DevTools'da daraxt bo'ylab yurish

Elements panelida ham "qadam tashlash" mumkin — va bu kodni yozishdan oldin tuzilmani tushunishga yordam beradi:

  • Klaviatura. Elements panelida elementni bosing. ↑ va ↓ — oldingi va keyingi qatorga, → — elementni ochish (bolalarini ko'rsatish), ← — yopish yoki otasiga o'tish.
  • "Non yo'li" (breadcrumbs). Panelning eng pastida tanlangan elementning ota-bobolari zanjiri turadi: html body main article.taom-karta p span.narx. Istalgan bo'g'inni bosib, o'sha otaga sakraysiz. Bu — parentElement zanjirining ko'rinishi.
  • Console'da $0. DOM daraxti darsidagi $0 bilan bugungi xususiyatlarni sinang: $0.parentElement, $0.nextElementSibling, $0.children.

Maslahat: Kodda uzun parentElement.parentElement zanjiri yozishdan oldin, breadcrumbs'ga qarang. Zanjir uzun bo'lsa, closest va ma'noli class yaxshiroq yo'l.

8. Ko'p uchraydigan xatolar

8.1 "Element" so'zini unutish

menyu.firstChild.textContent ko'pincha bo'sh joy beradi, nextSibling.classList esa undefined: matn tugunida classList yo'q. Keyingi qatorda .add(...) chaqirsangiz, konsolda Uncaught TypeError: Cannot read properties of undefined (reading 'add') chiqadi — "undefined ning xususiyatini o'qib bo'lmaydi (add ni o'qishda)". Xabar add ni ko'rsatadi, lekin sabab bir qadam oldinda: nextSibling element emas, matn tugunini bergan. Tuzatish: firstElementChild, nextElementSibling — nomida Element borini oling.

8.2 Oxirgi elementdan keyingisi

html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
</ul>
<script>
  const oxirgi = document.getElementById("menyu").lastElementChild;
  console.log(oxirgi.nextElementSibling.textContent);
</script>

Konsolda:

text
Uncaught TypeError: Cannot read properties of null (reading 'textContent')

Tarjimasi: "null ning xususiyatlarini o'qib bo'lmaydi ('textContent' ni o'qiyotganda)". Manti — oxirgi, undan keyingisi null. Xato textContent ni ko'rsatadi, lekin sababi bir qadam oldinda: nextElementSibling hech narsa topmagan. Tuzatish: chegarani tekshiring — if (keyingi !== null), ?. yoki «Keyingi taklif» misolidagi kabi ?? bilan zaxira.

8.3 O'chirilgan elementdan keyingisini so'rash

Tugagan taomlarni ro'yxatdan olib tashlamoqchimiz. Mana birinchi urinish:

html
<ul id="menyu">
  <li>Osh</li>
  <li class="tugagan">Manti</li>
  <li class="tugagan">Lag'mon</li>
  <li>Ko'k choy</li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  let taom = menyu.firstElementChild;
  while (taom !== null) {
    if (taom.matches(".tugagan")) {
      taom.remove();
    }
    taom = taom.nextElementSibling;
  }
  console.log(menyu.children.length);
</script>

Konsolda:

text
3

Ikkita taom qolishi kerak edi — uchta qoldi, Lag'mon o'chmadi. Nega? Manti o'chirilgach, u daraxtdan chiqib ketdi va endi uning qo'shnisi yo'q. taom.nextElementSibling — null, sikl erta tugadi.

Tuzatish: keyingisini o'chirishdan oldin eslab qoling:

html
<ul id="menyu">
  <li>Osh</li>
  <li class="tugagan">Manti</li>
  <li class="tugagan">Lag'mon</li>
  <li>Ko'k choy</li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  let taom = menyu.firstElementChild;
  while (taom !== null) {
    const keyingi = taom.nextElementSibling;
    if (taom.matches(".tugagan")) {
      taom.remove();
    }
    taom = keyingi;
  }
  console.log(menyu.children.length);
</script>

Konsolda:

text
2

Bu holatda statik ro'yxat ham yechim: for (const t of menyu.querySelectorAll(".tugagan")) t.remove(); — Elementlarni tanlash darsidagi qoida.

8.4 children da forEach qidirish

Elementlarni tanlash darsida NodeList da forEach borligini ko'rdingiz. children ham ro'yxatga o'xshaydi — demak unda ham bor, deb o'ylash tabiiy:

html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  menyu.children.forEach((taom) => console.log(taom.textContent));
</script>

Konsolda:

text
Uncaught TypeError: menyu.children.forEach is not a function

Tarjimasi: "menyu.children.forEach — funksiya emas". children — HTMLCollection, unda forEach umuman yo'q (undefined), uni chaqirib bo'lmaydi. NodeList va HTMLCollection — ikki xil to'plam, imkoniyatlari ham har xil. Tuzatish: for...of ishlating — u ikkalasida ham ishlaydi. Yoki [...menyu.children] bilan haqiqiy massivga aylantiring.

8.5 Uzun parentElement zanjiri

tugma.parentElement.parentElement.parentElement — HTML'ga bitta o'ram qo'shilsa, jimgina boshqa element oladi («Zanjir va uning xavfi» bo'limi). Tuzatish: ikki qadamdan ko'p bo'lsa — closest(".ma'noli-class").

9. Mashqlar

1-mashq (oson): Qarindoshlarni toping

#ichimliklar ro'yxatida konsolga chiqaring: (1) birinchi ichimlik matnini; (2) oxirgi ichimlik matnini; (3) "Kompot" dan oldingi ichimlik matnini; (4) "Kompot" ning otasi bo'lgan elementni.

Ishora: "Kompot" ni id bilan toping, qolganini bugungi xususiyatlar bilan — querySelector siz.

html
<ul id="ichimliklar">
  <li>Ko'k choy</li>
  <li>Qora choy</li>
  <li id="kompot">Kompot</li>
  <li>Ayron</li>
</ul>
<script>
  // shu yerga yozing
</script>
Yechim
html
<ul id="ichimliklar">
  <li>Ko'k choy</li>
  <li>Qora choy</li>
  <li id="kompot">Kompot</li>
  <li>Ayron</li>
</ul>
<script>
  const kompot = document.getElementById("kompot");
  const royxat = kompot.parentElement;
  console.log(royxat.firstElementChild.textContent);
  console.log(royxat.lastElementChild.textContent);
  console.log(kompot.previousElementSibling.textContent);
  console.log(royxat);
</script>

Konsolda:

text
Ko'k choy
Ayron
Qora choy
<ul id="ichimliklar">…</ul>

Avval otani topdik, keyin undan birinchi va oxirgi bolaga tushdik. Qora choy va Ayron narxlari «Bahor» menyusida yo'q — bu mashq uchun ro'yxat.

2-mashq (o'rta): Narx yonidagi nom

Har qatorda ikkita span: avval nom, keyin narx. Faqat .narx larni querySelectorAll bilan oling, nomni esa har narxning oldingi qo'shnisidan o'qing. Natija: Osh — 35 000, Manti — 30 000, Lag'mon — 28 000.

html
<ul id="menyu">
  <li><span>Osh</span> <span class="narx">35 000</span></li>
  <li><span>Manti</span> <span class="narx">30 000</span></li>
  <li><span>Lag'mon</span> <span class="narx">28 000</span></li>
</ul>
<script>
  // shu yerga yozing
</script>
Yechim
html
<ul id="menyu">
  <li><span>Osh</span> <span class="narx">35 000</span></li>
  <li><span>Manti</span> <span class="narx">30 000</span></li>
  <li><span>Lag'mon</span> <span class="narx">28 000</span></li>
</ul>
<script>
  for (const narx of document.querySelectorAll(".narx")) {
    const nom = narx.previousElementSibling.textContent;
    console.log(`${nom} — ${narx.textContent}`);
  }
</script>

Konsolda:

text
Osh — 35 000
Manti — 30 000
Lag'mon — 28 000

Ikki span orasida bo'sh joy bor — u matn tuguni. previousSibling o'sha bo'sh joyni qaytarardi, previousElementSibling esa uni o'tkazib yubordi.

3-mashq (qiyin): Nechanchi o'rinda?

orniniTop(element) funksiyasini yozing: u element otasining bolalari orasida nechanchi ekanini qaytarsin (birinchisi — 1). Ichida children yoki indexOf ishlatmang — faqat previousElementSibling va while. Lag'mon va Osh uchun tekshiring: Lag'mon — 3-o'rinda, Osh — 1-o'rinda.

Ishora: hisoblagich 1 dan boshlanadi; har orqaga qadamda bittaga oshadi; null ga yetganda to'xtaydi.

html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
  <li id="lagmon">Lag'mon</li>
  <li>Ko'k choy</li>
</ul>
<script>
  // shu yerga yozing
</script>
Yechim
html
<ul id="menyu">
  <li>Osh</li>
  <li>Manti</li>
  <li id="lagmon">Lag'mon</li>
  <li>Ko'k choy</li>
</ul>
<script>
  function orniniTop(element) {
    let orin = 1;
    let oldingi = element.previousElementSibling;
    while (oldingi !== null) {
      orin++;
      oldingi = oldingi.previousElementSibling;
    }
    return orin;
  }

  const lagmon = document.getElementById("lagmon");
  const osh = lagmon.parentElement.firstElementChild;
  console.log(`${lagmon.textContent} — ${orniniTop(lagmon)}-o'rinda`);
  console.log(`${osh.textContent} — ${orniniTop(osh)}-o'rinda`);
</script>

Konsolda:

text
Lag'mon — 3-o'rinda
Osh — 1-o'rinda

Osh uchun sikl bir marta ham aylanmadi: uning oldingisi darhol null, shuning uchun 1 qaytdi. Funksiya ro'yxat uzunligini bilmaydi va bilishi shart ham emas.

4-mashq: Portfolio qadami — "Keyingi sahifa" havolasi

Portfolio menyusida joriy sahifa havolasi aria-current="page" bilan belgilangan. Har sahifa oxiriga menyudagi keyingi sahifaga havola qo'shing: "Keyingi sahifa: Loyihalar". Menyudagi oxirgi sahifada havola chiqmasin. asosiy.js ga keyingiSahifaniKorsat funksiyasini yozing:

  1. Asosiy menyudan joriy havolani toping. Topilmasa (404.html da menyu yo'q) — funksiya tugasin.
  2. Havoladan uning li siga, li dan keyingi li ga o'ting. Keyingisi null bo'lsa — tugasin.
  3. Keyingi li ning birinchi element bolasi — havola. Undan href atributi va matnini oling.
  4. p va a yarating, main oxiriga qo'shing (Element yaratish).
Yechim

asosiy.js ga:

js
function keyingiSahifaniKorsat() {
  const joriy = document.querySelector(
    'nav[aria-label="Asosiy menyu"] [aria-current="page"]',
  );
  if (joriy === null) {
    return;
  }
  const keyingiLi = joriy.parentElement.nextElementSibling;
  if (keyingiLi === null) {
    return;
  }
  const manba = keyingiLi.firstElementChild;
  const havola = document.createElement("a");
  havola.href = manba.getAttribute("href");
  havola.textContent = manba.textContent;

  const qator = document.createElement("p");
  qator.append("Keyingi sahifa: ", havola);
  document.querySelector("main").append(qator);
}

keyingiSahifaniKorsat();

Menyuga o'xshash parchada tekshiring:

html
<nav aria-label="Asosiy menyu">
  <ul>
    <li><a href="./">Bosh sahifa</a></li>
    <li><a href="haqimda/" aria-current="page">Men haqimda</a></li>
    <li><a href="loyihalar/">Loyihalar</a></li>
    <li><a href="aloqa/">Aloqa</a></li>
  </ul>
</nav>
<main>
  <h1>Men haqimda</h1>
</main>
<script>
  function keyingiSahifaniKorsat() {
    const joriy = document.querySelector(
      'nav[aria-label="Asosiy menyu"] [aria-current="page"]',
    );
    if (joriy === null) {
      return;
    }
    const keyingiLi = joriy.parentElement.nextElementSibling;
    if (keyingiLi === null) {
      return;
    }
    const manba = keyingiLi.firstElementChild;
    const havola = document.createElement("a");
    havola.href = manba.getAttribute("href");
    havola.textContent = manba.textContent;

    const qator = document.createElement("p");
    qator.append("Keyingi sahifa: ", havola);
    document.querySelector("main").append(qator);
  }
  keyingiSahifaniKorsat();
  console.log(document.querySelector("main").lastElementChild);
</script>

Konsolda:

text
<p>…</p>

Natija tabida sarlavha ostida "Keyingi sahifa: Loyihalar" chiqdi. Konsolda p ning ichi … bilan qisqartirildi — unda havola bor. aria-current="page" ni aloqa/ havolasiga ko'chiring ("Tahrirlab ko'rish"): havola chiqmaydi, chunki Aloqa — oxirgi.

Uch nozik joy:

  • Nega parentElement, closest emas? Portfolio menyusida a doim li ning bevosita bolasi — tuzilma aniq, bir qadam. closest("li") ham to'g'ri bo'lardi.
  • Nega getAttribute("href")? Menyudagi yo'llar nisbiy (../aloqa/) va har sahifada to'g'ri yozilgan. Atributni o'zgarishsiz ko'chirsak, yangi havola ham to'g'ri ishlaydi. manba.href xususiyati to'liq manzil qaytaradi — u ham ishlardi (Atributlar va xususiyatlar).
  • Telefonda menyu popover ichida yashiringan (Navbar va mobil menyu). DOM'da u baribir bor, shuning uchun querySelector uni topadi — ko'rinmaslik elementni daraxtdan olib tashlamaydi.

Commit: JS: sahifa oxiriga keyingi sahifa havolasini qo'sh.

10. Real ishda

  • Akkordeon va savol-javob bloklari. Savol tugmasi bosilganda uning nextElementSibling — javob paneli ochiladi. Ko'p saytlardagi "Ko'p beriladigan savollar" bo'limi aynan shunday yozilgan.
  • Jadvallar. Katakdan qatorga (parentElement), qatordan keyingi qatorga (nextElementSibling) — Excel'ga o'xshash jadvallarda strelka tugmalari bilan yurish shu xususiyatlar ustiga quriladi.
  • Hodisalar bilan. Event delegation darsida bitta tinglovchi butun ro'yxatni boshqaradi va bosilgan joydan closest bilan kerakli qatorga chiqadi. Bugungi "yuqoriga yurish" o'shanda har kuni kerak bo'ladi.
  • Freymvorklarda (React kabi — kursda alohida o'rganamiz) qo'lda yurish kamroq, chunki elementlar ma'lumotdan chiziladi. Lekin tashqi kutubxonani ulaganda yoki sahifani avtomatik testlashda DOM bo'ylab yurish baribir kerak.
  • Intervyuda: "children va childNodes farqi?", "parentNode va parentElement qachon farq qiladi?", "nextSibling nega matn qaytaradi?" — boshlang'ich darajadagi odatiy savollar.

Xulosa

  • Har yo'nalish uchun ikki xususiyat: tugunlik (firstChild, nextSibling) va elementlik (firstElementChild, nextElementSibling); deyarli doim elementlik kerak.
  • Pastga — children, firstElementChild, lastElementChild; yonga — previousElementSibling, nextElementSibling; yuqoriga — parentElement.
  • Qarindosh yo'q bo'lsa — null; bu sikl uchun "oxiri" signali va Cannot read properties of null xatosining manbai.
  • Elementni o'chirishdan oldin keyingisini eslab qoling — o'chirilgan elementning qo'shnisi yo'q.
  • Uzoqdagi ota — closest, "ichidami?" — contains, butun daraxt — rekursiya.

Keyingi dars: Ko'p elementni samarali chizish — o'nlab va minglab elementni sahifaga bittalab emas, bir martada qo'shish: DocumentFragment, <template> va replaceChildren.

Manbalar

  • MDN: "Element: parentElement", "Element: children", "Element: nextElementSibling", "Node: contains()" — developer.mozilla.org
  • MDN: "Whitespace in the DOM" — developer.mozilla.org
  • DOM Standard: "Interface ParentNode", "Interface NonDocumentTypeChildNode" — dom.spec.whatwg.org
  • Chrome DevTools: "Elements panel: keyboard navigation" — developer.chrome.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
DOM bo'ylab harakatlanish: parentElement, children, nextElementSibling va closest — IlmHamroh