DOM ağacında bir öğeden ebeveynine çıkmak için parentElement, çocuk öğelere inmek için children, kardeş öğeler arasında dolaşmak için nextElementSibling ve previousElementSibling kullanılır. Bu özelliklerin parentNode, childNodes, nextSibling gibi "Node" sürümleri de vardır; aralarındaki fark, metin ve yorum düğümlerini de sayıp saymamalarıdır. En yakın uygun atayı bulmak için closest(), karmaşık gezintiler için TreeWalker kullanılır.

Bu yazıda öğe ağacı ile düğüm ağacı arasındaki farkı, boşluk metin düğümlerinin yarattığı sürprizleri, NodeList ve HTMLCollection karşılaştırmasını, canlı koleksiyonlarda döngü tuzağını ve TreeWalker/NodeIterator ile filtreli gezinmeyi MDN belgelerine göre anlatıyoruz. Seçici tabanlı arama için querySelector ile öğe seçme yazısına, serinin tamamı için Web API öğrenme rehberi sayfasına bakabilirsiniz.

Düğüm ağacı ve öğe ağacı

Düğüm ağacı ve öğe ağacı: Ebeveyn, Çocuklar, İlk ve son çocuk, Çocuk sayısı

MDN'in DOM anatomisi rehberi, ağaçta gezinmek için iki özellik ailesi tanımlar. Node arayüzündeki özellikler her tür düğümü (öğe, metin, yorum) hesaba katar. Öğe düğümleri belgenin omurgasını oluşturduğu için DOM ayrıca yalnızca öğeleri dolaşan, metin ve yorum düğümlerini atlayan özellikler de sunar. Hangisini kullandığınız, aldığınız sonucu doğrudan değiştirir:

YönTüm düğümler (Node)Yalnızca öğeler
EbeveynparentNodeparentElement
ÇocuklarchildNodes (canlı NodeList)children (canlı HTMLCollection)
İlk ve son çocukfirstChild, lastChildfirstElementChild, lastElementChild
Çocuk sayısıchildNodes.lengthchildElementCount
Önceki kardeşpreviousSiblingpreviousElementSibling
Sonraki kardeşnextSiblingnextElementSibling

Öğe sürümleri her yerde aynı arayüzde tanımlı değildir. parentElement tüm düğümlerde bulunur. children, firstElementChild, lastElementChild ve childElementCount çocuk alabilen Document, DocumentFragment ve Element düğümlerinde vardır. previousElementSibling ve nextElementSibling ise Element ve CharacterData (metin ve yorum düğümleri) üzerinde bulunur.

Boşluk metin düğümleri neden sorun çıkarır?

MDN, tarayıcıların kaynak işaretlemedeki boşlukları temsil etmek için belgeye metin düğümleri eklediğini birkaç sayfada tekrarlar. Etiketler arasındaki satır sonu ve girinti de bir Text düğümüdür. Bu yüzden childNodes[0], firstChild veya nextSibling ile beklediğiniz öğe yerine bir boşluk düğümü alabilirsiniz.

html
<ul id="list">
  <li>First (1)</li>
  <li>Second (2)</li>
  <li>Third (3)</li>
</ul>
javascript
const list = document.getElementById("list");
console.log(list.firstElementChild.textContent);
// logs "First (1)"

Aynı listede list.firstChild, <ul> ile ilk <li> arasındaki satır sonunu ve girintiyi taşıyan bir metin düğümüdür. firstElementChild bu düğümü atlar. MDN'in nextSibling sayfası da aynı nedenle nextElementSibling kullanmanızı önerir; bu özellik boşluk düğümlerini, öğeler arasındaki diğer metinleri ve yorumları atlayarak bir sonraki öğeyi verir. Metin düğümlerine özellikle ihtiyacınız yoksa öğe sürümlerini varsayılan olarak kullanmak bu sınıf hataları baştan önler.

Ebeveyne çıkmak: parentNode ve parentElement

parentNode, düğümün ebeveynini türüne bakmadan döndürür; bir öğenin ebeveyni bir Element, Document veya DocumentFragment olabilir. Document ve DocumentFragment düğümlerinin hiçbir zaman ebeveyni olmaz. Yeni oluşturulmuş ve ağaca eklenmemiş bir düğümde de parentNode null'dır. parentElement ise yalnızca ebeveyn bir öğeyse onu döndürür, değilse null verir. MDN farkı kök öğe üzerinden gösterir:

javascript
const html = document.querySelector("html");
console.log(html.parentElement); // null
console.log(html.parentNode); // document

Pratikte bir öğenin üzerindeki kapsayıcıya ulaşmak için parentElement daha güvenlidir, çünkü sonuç ya bir öğedir ya null; style veya classList gibi öğe üyelerini kullanmadan önce yalnızca null kontrolü yapmanız yeterlidir. Birkaç kat yukarıdaki belirli bir atayı arıyorsanız parentElement zinciri kurmak yerine closest() kullanın.

Çocuklara inmek: childNodes ve children

childNodes, öğeler, metinler ve yorumlar dahil tüm çocuk düğümleri canlı bir NodeList olarak verir. MDN, aynı öğe üzerinde childNodes'a birden çok kez erişmenin aynı NodeList nesnesini döndürdüğünü not eder. Liste canlı olduğu için çocuk eklendikçe veya kaldırıldıkça kendiliğinden güncellenir:

javascript
const parent = document.getElementById("parent");
let childNodes = parent.childNodes;
console.log(childNodes.length); // let's assume "2"
parent.appendChild(document.createElement("div"));
console.log(childNodes.length); // outputs "3"

children ise yalnızca çocuk öğeleri canlı bir HTMLCollection olarak döndürür. Çocuk öğe yoksa length değeri 0 olan boş bir koleksiyondur. Öğelere item() ile veya dizi gibi köşeli parantezle erişebilir, for...of ile dönebilirsiniz. Bir öğenin herhangi bir çocuğu olup olmadığını öğrenmek için hasChildNodes(), yalnızca çocuk öğe sayısı için childElementCount kullanılır.

javascript
const myElement = document.getElementById("foo");
for (const child of myElement.children) {
  console.log(child.tagName);
}

Kardeşler arasında dolaşmak

nextElementSibling, ebeveynin children listesinde hemen sonra gelen öğeyi, son öğedeyse null döndürür; previousElementSibling bir öncekini verir. Bir döngüyle tüm sonraki kardeşleri dolaşabilirsiniz. MDN'in örneği:

html
<div id="div-01">Here is div-01</div>
<div id="div-02">Here is div-02</div>
javascript
let el = document.getElementById("div-01").nextElementSibling;
console.log("Siblings of div-01:");
while (el) {
  console.log(el.nodeName);
  el = el.nextElementSibling;
}

MDN, sayfa yüklendiğinde konsola sırasıyla "Siblings of div-01:", "DIV" ve "SCRIPT" yazıldığını gösterir. "SCRIPT" satırı, örneği çalıştıran <script> öğesinin de <body> içinde bir kardeş olmasından gelir. Bu küçük ayrıntı, kardeş dolaşırken yalnızca beklediğiniz öğelerin değil, aynı ebeveyn altındaki her öğenin geleceğini hatırlatır. Belirli türdeki kardeşleri istiyorsanız döngü içinde matches() ile filtreleyin.

Yukarı doğru arama: closest()

element.closest(selectors), öğenin kendisinden başlayarak belge köküne doğru ebeveynleri dolaşır ve verilen CSS seçiciyle eşleşen ilk öğeyi döndürür; eşleşme yoksa null verir. Seçici geçersizse SyntaxError fırlatılır. MDN'in örneği hem öğenin kendisinin de test edildiğini hem de seçicinin tüm belge bağlamında değerlendirildiğini gösterir:

html
<article>
  <div id="div-01">
    Here is div-01
    <div id="div-02">
      Here is div-02
      <div id="div-03">Here is div-03</div>
    </div>
  </div>
</article>
javascript
const el = document.getElementById("div-03");

// the closest ancestor with the id of "div-02"
console.log(el.closest("#div-02")); // <div id="div-02">

// the closest ancestor which is a div in a div
console.log(el.closest("div div")); // <div id="div-03">

// the closest ancestor which is a div and has a parent article
console.log(el.closest("article > div")); // <div id="div-01">

// the closest ancestor which is not a div
console.log(el.closest(":not(div)")); // <article>

İkinci satırda sonuç div-03'ün kendisidir, çünkü o da bir div içindeki bir div'dir ve arama öğenin kendisinden başlar. closest(), olay delegasyonunda tıklanan iç öğeden ilgili satıra veya karta ulaşmak için en sık kullanılan araçtır; bu deseni olay delegasyonu yazısında anlattık.

İçerme ve kök: contains() ve getRootNode()

node.contains(otherNode), verilen düğüm bu düğümün kendisi, bir çocuğu, çocuğunun çocuğu veya daha alttaki bir torunu ise true döndürür. MDN, bir düğümün kendi içinde "içerildiğini" özellikle not eder; parametre null ise sonuç her zaman false'tur. MDN'in örneği bu kapsayıcılığı hesaba katar:

javascript
function isInPage(node) {
  return node === document.body ? false : document.body.contains(node);
}

node.getRootNode(), ebeveyn bağlantılarını izleyerek düğümün bağlı olduğu ağacın kökünü verir. Normal bir sayfadaki öğe için bu, belgenin kendisidir; shadow DOM içindeki bir öğe için ilgili ShadowRoot'tur; hiçbir belgeye bağlı olmayan bir öğe için ise ait olduğu ağacın köküdür. { composed: true } seçeneği shadow root'un ötesine geçip asıl belge köküne ulaşır. Varsayılan değer false'tur. Shadow DOM için Web Components ve shadow DOM yazısına bakabilirsiniz. İki düğümün ağaç sırasındaki konumunu karşılaştıran compareDocumentPosition() ise DOM nedir yazısında.

NodeList ile HTMLCollection farkı

NodeList ile HTMLCollection farkı: İçerik, Nereden gelir, Canlı mı, forEach, entries, keys, values

Gezinme özellikleri ve seçme metotları iki tür koleksiyon döndürür. İkisi de dizi benzeridir (length, indeksle erişim, yinelenebilme) ama gerçek dizi değildir ve değiştirilemez. Farkları MDN'e göre şöyledir:

ÖzellikNodeListHTMLCollection
İçerikHer tür düğümYalnızca öğeler
Nereden gelirchildNodes, querySelectorAll, getElementsByNamechildren, getElementsByClassName, getElementsByTagName, document.forms
Canlı mıÇoğunlukla canlı, querySelectorAll statikHer zaman canlı
forEach, entries, keys, valuesVarYok
namedItem ve adla erişimYokVar, id veya name ile
item(i) aralık dışındanullnull
[i] aralık dışındaundefinedundefined

MDN'in NodeList sayfası, document.querySelectorAll() metodunun statik NodeList döndüren tek API olduğunu, diğer çoğu durumda listenin canlı olduğunu belirtir. HTMLCollection ise bir anahtar-değer haritası gibi de davranır: document.forms.myForm veya document.forms.namedItem("myForm") ile kimliği myForm olan formu alabilirsiniz. Kimlikte nokta gibi karakterler varsa köşeli parantez gerekir. MDN ayrıca HTMLCollection'ın tamamen sayılardan oluşan kimlikleri tanımadığını not eder, çünkü bu dizi tarzı erişimle çakışır.

Canlı koleksiyonda döngü tuzağı

Canlı bir koleksiyon üzerinde dönerken onun üyeliğini veya sırasını değiştiren bir işlem yaparsanız indeksler ve uzunluk döngü sırasında değişir. MDN'in seçme ve gezinme rehberindeki örnek, her öğeden note sınıfını kaldıran bir döngünün bazı öğeleri atladığını gösterir: kaldırılan öğe koleksiyondan çıkar, sonrakiler bir sıra kayar ve döngü bir sonrakini ziyaret etmez. Aynı sorun childNodes üzerinde dönerken düğüm silmede de yaşanır.

javascript
const notes = document.getElementsByClassName("note");

// Hatalı: döngü sırasında koleksiyon küçülür, öğeler atlanır
for (const note of notes) {
  note.classList.remove("note");
}

// Doğru: anlık bir kopya üzerinde dön
for (const note of Array.from(notes)) {
  note.classList.remove("note");
}

MDN'in önerdiği çözümler bir anlık görüntü üzerinde dönmek (Array.from() veya querySelectorAll()) ya da üyeliği değiştiren işlemleri döngü bitene kadar ertelemektir. Tüm çocukları silmek için ise while (box.firstChild) box.removeChild(box.firstChild) kalıbı canlı listenin her çağrıda yeniden indekslenmesinden yararlanır; modern karşılığı box.replaceChildren()'dır. Canlı koleksiyonun sonraki değişiklikleri izleme özelliğini kullanmak istiyorsanız referansını yine saklayabilirsiniz.

TreeWalker ve NodeIterator ile filtreli gezinme

TreeWalker ve NodeIterator ile filtreli gezinme: root, whatToShow, filter

querySelectorAll("*") ile bir alt ağaçtaki tüm öğeleri dolaşabilirsiniz, ama MDN bunun sınırlı olduğunu belirtir: metin ve yorum gibi öğe olmayan düğümleri ziyaret edemez, belirli bir alt ağacı karmaşık seçiciler yazmadan atlayamazsınız. Genel gezinme için DOM iki arayüz sunar: NodeIterator ve TreeWalker. Bunlar document.createNodeIterator() ve document.createTreeWalker() ile oluşturulur ve aynı üç argümanı alır:

  • root: gezintinin başladığı kök düğüm.
  • whatToShow: hangi düğüm türlerinin ziyaret edileceğini belirten bit maskesi. Varsayılan değer NodeFilter.SHOW_ALL'dur. Sabitler bit düzeyinde VEYA ile birleştirilebilir: NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT.
  • filter: bir fonksiyon veya acceptNode(node) metodu olan bir nesne. NodeFilter.FILTER_ACCEPT düğümü döndürür, FILTER_SKIP düğümü atlar ama torunlarını değerlendirmeye devam eder, FILTER_REJECT ise TreeWalker'da düğümü tüm torunlarıyla birlikte atlar. NodeIterator'da FILTER_REJECT, FILTER_SKIP gibi davranır.

NodeIterator filtreden geçen düğümleri ağaç sırasında doğrusal bir liste gibi sunar: nextNode() ileri, previousNode() geri gider ve uygun düğüm kalmadığında null döner. İlk nextNode() çağrısı, filtreden geçiyorsa kökün kendisini döndürür. TreeWalker ise filtrelenmiş görünümde ağaç ilişkilerini korur: parentNode(), firstChild(), lastChild(), previousSibling(), nextSibling(), previousNode() ve nextNode() metotlarıyla gezinir. Konumu, yazılabilir currentNode özelliğinde tutulur ve başlangıçta köke eşittir; NodeIterator'dan farklı olarak ilk nextNode() çağrısı kökü döndürmez. Uygun düğüm bulunamazsa metotlar null döndürür ve currentNode değişmez.

MDN'in örneği, data-skip özniteliği taşıyan alt ağaçları dışarıda bırakarak boşluk olmayan metin düğümlerini toplar:

html
<article id="article">
  <p>Read <strong>this</strong> paragraph.</p>
  <aside data-skip><p>Ignore this note.</p></aside>
  <p>Read this paragraph too.</p>
</article>
javascript
const article = document.getElementById("article");
const walker = document.createTreeWalker(
  article,
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
  (node) => {
    if (node.nodeType === Node.ELEMENT_NODE) {
      return node.hasAttribute("data-skip")
        ? NodeFilter.FILTER_REJECT
        : NodeFilter.FILTER_SKIP;
    }
    return node.data.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
  },
);

const parts = [];
let node;
while ((node = walker.nextNode())) {
  parts.push(node.data);
}
console.log(parts.join(""));
// "Read this paragraph.Read this paragraph too."

Örnekte yalnızca metin toplansa da SHOW_ELEMENT gereklidir. MDN'in açıkladığı gibi whatToShow ile eşleşmeyen düğümler filtreye hiç gönderilmez. Yalnızca SHOW_TEXT verilseydi <aside> öğesi filtreye ulaşmadan atlanır, içindeki metin ise yine ziyaret edilirdi. Öğeleri filtreye dahil edip FILTER_REJECT döndürmek, alt ağacın tamamını budar. NodeIterator ağaç yapısını bilmediği için böyle bir budama yapamaz. Ayrıca SHOW_ATTRIBUTE sabiti yalnızca kök bir öznitelik düğümüyse etkilidir; öznitelikleri dolaşmak için element.attributes kullanılır.

Mini örnek: tablodaki satırı tıklamayla bulmak

Gezinme özelliklerini olay işleyicisiyle birleştiren kısa bir örnek: tablonun herhangi bir hücresine tıklandığında o satırın kaçıncı satır olduğunu ve ilk hücresindeki adı bulalım.

html
<table id="liste">
  <tbody>
    <tr><td>alanadi.com.tr</td><td><button type="button">Yenile</button></td></tr>
    <tr><td>ornek.com</td><td><button type="button">Yenile</button></td></tr>
  </tbody>
</table>
<p id="bilgi"></p>
javascript
const tablo = document.getElementById("liste");

tablo.addEventListener("click", (event) => {
  const satir = event.target.closest("tr");
  if (!satir || !tablo.contains(satir)) return;

  const sira = Array.from(satir.parentElement.children).indexOf(satir) + 1;
  const ad = satir.firstElementChild.textContent;
  document.getElementById("bilgi").textContent = `${sira}. satır: ${ad}`;
});

closest("tr") tıklanan düğmeden satıra çıkar; contains() satırın gerçekten bu tablonun içinde olduğunu doğrular. parentElement.children satırın kardeşlerini canlı bir koleksiyon olarak verir ve Array.from() ile diziye çevrildiği için indexOf() kullanılabilir. firstElementChild ise satır içindeki olası boşluk metin düğümlerini atlayarak doğrudan ilk hücreyi verir.

Sık yapılan hatalar

  • firstChild ile öğe beklemek: Boşluk metin düğümü dönebilir; firstElementChild kullanın.
  • parentNode'un her zaman öğe olduğunu sanmak: Kök öğede parentNode belgeyi, parentElement null'ı verir.
  • Canlı koleksiyonu döngüde değiştirmek: Öğe atlanır; önce diziye çevirin.
  • HTMLCollection'da forEach çağırmak: Bu metot yalnızca NodeList'te vardır; for...of veya Array.from() kullanın.
  • NodeList'i for...in ile dolaşmak: length ve item gibi özellikler de döngüye girer.
  • closest()'in öğenin kendisini de test ettiğini unutmak: Arama öğenin kendisinden başlar; yalnızca ataları istiyorsanız el.parentElement?.closest(...) yazın.

Sık Sorulan Sorular

children ile childNodes arasındaki fark nedir?

children yalnızca öğe düğümlerini içeren canlı bir HTMLCollection, childNodes ise metin ve yorumlar dahil tüm çocuk düğümleri içeren canlı bir NodeList döndürür. Çoğu arayüz kodunda ihtiyacınız olan children'dır.

Bir öğenin kaçıncı çocuk olduğunu nasıl bulurum?

DOM'da doğrudan bir indeks özelliği yoktur. Ebeveynin children koleksiyonunu Array.from() ile diziye çevirip indexOf(öğe) çağırabilirsiniz; yukarıdaki mini örnek bunu gösteriyor.

TreeWalker ne zaman gerekir?

Metin düğümlerini dolaşmanız, belirli alt ağaçları tümüyle atlamanız veya filtrelenmiş görünümde ebeveyn ve kardeş ilişkileriyle gezinmeniz gerektiğinde. Yalnızca öğeleri seçmek için querySelectorAll() genellikle yeterlidir.

NodeList neden bazen canlı bazen statik?

MDN'e göre NodeList'in iki türü vardır. childNodes gibi çoğu kaynak canlı liste döndürür; querySelectorAll() ise çağrı anının anlık görüntüsünü veren statik bir liste döndürür. Listenin uzunluğunu önbelleğe alıp almayacağınıza karar verirken bu farkı göz önünde tutun.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · öğe seçme · DOM nedir · olay delegasyonu · MutationObserver ile DOM değişikliklerini izleme