Mert Özen Her Satırda Daha İleri
Frontend JavaScript Orta — #12

JavaScript ile Gelişmiş DOM: Gezinme, closest, dataset, template ve Toplu Güncelleme

Mert Özen 3 Kas 2026 9 dk 4 görüntülenme
JavaScript ile Gelişmiş DOM: Gezinme, closest, dataset, template ve Toplu Güncelleme

DOM ağacında parent/children/sibling ve closest ile geziniyor; dataset, insertAdjacentHTML, DocumentFragment, template, cloneNode ve getBoundingClientRect'i görüyor, reflow'a girip toplu güncelleme yapıyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Bir butonun üzerinde iki data- attribute'u var. Aşağıdaki dosyayı açtığında konsola hangi iki satır yazılır?

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Predict</title>
</head>
<body>
  <button id="buy" data-product-id="42" data-in-stock="false">Buy</button>

  <script>
    const button = document.querySelector("#buy");
    console.log(button.dataset.productId + 1);
    console.log(button.dataset.inStock ? "in stock" : "sold out");
  </script>
</body>
</html>

Okumaya devam etmeden önce bir tahmin yap. İki satırın ikisinde de küçük bir tuzak var ve ikisi de Temel seride gördüğümüz bir kurala dayanıyor. Cevabı dataset'i anlattıktan sonra konuşacağız.

DOM Ağacında Gezinmek

Temel seride element'leri querySelector ile bulduk. Ama çoğu zaman elinde zaten bir element vardır ve ona göre konumlanmış başka bir element'e ulaşmak istersin: kapsayıcısına, yanındakine, içindekilere. DOM bir aile ağacı gibidir. Her element'in bir ebeveyni (parent), çocukları (children) ve kardeşleri (sibling) vardır. Bu dosyayı kaydedip aç ve konsola bak:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>DOM navigation</title>
</head>
<body>
  <ul id="menu">
    <li>Home</li>
    <li class="active">Blog</li>
    <li>Contact</li>
  </ul>

  <script>
    const active = document.querySelector(".active");

    console.log(active.parentElement.id);                    // "menu"
    console.log(active.previousElementSibling.textContent);  // "Home"
    console.log(active.nextElementSibling.textContent);      // "Contact"

    const menu = document.querySelector("#menu");
    console.log(menu.firstElementChild.textContent); // "Home"
    console.log(menu.lastElementChild.textContent);  // "Contact"
    console.log(menu.children.length);               // 3
    console.log(menu.childNodes.length);             // 7
  </script>
</body>
</html>

Son iki satır ilk bakışta tuhaf. Menüde üç li var ama childNodes yedi diyor. Sebebi, DOM'da sadece element'lerin değil, metin node'larının da olması. HTML'de li'lerin arasına koyduğun satır sonu ve girinti boşlukları da birer metin node'u olarak ağaca girer. children sadece element'leri sayar, childNodes ise hepsini.

Aynı ayrım kardeşlerde de geçerli. nextSibling çoğu zaman sana o boşluk metnini verir, nextElementSibling ise bir sonraki gerçek element'i. Günlük işlerde neredeyse her zaman adında "Element" geçen versiyonları kullanırsın: parentElement, children, firstElementChild, lastElementChild, previousElementSibling, nextElementSibling. Bir yönde gidecek element kalmadığında hepsi null döndürür.

closest: Yukarı Doğru Arama

Gezinmenin en kullanışlı aracı closest'tır. Element'in kendisinden başlar, verdiğin selector'a uyan ilk element'i bulana kadar ağaçta yukarı çıkar. Bulamazsa null döndürür. Bu dosyayı kaydet; bir sonraki bölümde de aynı sayfayı kullanacağız:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>closest</title>
</head>
<body>
  <article class="card" data-id="7">
    <h3>Mechanical keyboard</h3>
    <div class="actions">
      <button class="delete">Delete</button>
    </div>
  </article>

  <script>
    const deleteButton = document.querySelector(".delete");

    const card = deleteButton.closest(".card");
    console.log(card.dataset.id);                  // "7"
    console.log(deleteButton.closest("button") === deleteButton); // true
    console.log(deleteButton.closest(".sidebar")); // null
  </script>
</body>
</html>

Asansör gibi düşün: bulunduğun kattan başlarsın ve aradığın tabelayı görene kadar yukarı çıkarsın. Bir karttaki "Delete" butonuna tıklandığında hangi kartın silineceğini bulmak, bir menü element'inin hangi menüye ait olduğunu anlamak gibi işler için idealdir. closest'ın asıl parladığı yer, bir sonraki yazıda göreceğimiz event delegation olacak.

dataset: Veriyi Element Üzerinde Taşımak

HTML'de adı data- ile başlayan her attribute senin özel verindir. Tarayıcı bunlara bir anlam yüklemez; JavaScript'ten dataset ile okuyup yazarsın. İsimlendirme kuralı şu: data- öneki düşer, tireden sonra gelen harf büyür. data-product-id, dataset.productId olur. Bunu closest örneğindeki sayfanın konsolunda dene:

const card = document.querySelector(".card");

console.log(card.dataset.id);        // "7"
card.dataset.status = "sold";        // HTML'e data-status="sold" eklenir
card.dataset.lastViewed = "today";   // data-last-viewed="today"
delete card.dataset.status;          // attribute silinir

console.log(Number(card.dataset.id) + 1); // 8

Kuralın en önemli kısmı şu: dataset'teki her değer string'dir. Okurken string gelir, yazarken ne verirsen ver string'e çevrilir.

Şimdi baştaki soruya dönelim. button.dataset.productId, 42 sayısı değil "42" string'idir. String ile sayıyı + ile buluşturduğunda toplama değil birleştirme olur: sonuç "421". İkinci satırda dataset.inStock, false değil "false" string'idir. Boş olmayan her string truthy olduğu için ternary "in stock" yazar. Cevap: 421 ve in stock. Doğrusu, sayı için Number(...) kullanmak, boolean için de dataset.inStock === "true" diye açıkça karşılaştırmak.

data- attribute'larının bir güzel yanı da CSS'ten erişilebilmeleri. .card[data-status="sold"] gibi bir selector'la satılmış kartları soluklaştırabilir, durum değiştiğinde sadece dataset.status'ü güncelleyerek görünümü değiştirebilirsin.

insertAdjacentHTML ve Kardeşleri

Temel seride innerHTML ile bir element'in içeriğini baştan yazmayı gördük. Bazen ise mevcut içeriğe dokunmadan belirli bir noktaya ekleme yapmak istersin. insertAdjacentHTML dört konumdan birine HTML ekler. İlk bloğu boş bir HTML dosyasının body'sine koy, ikinci bloğu o sayfanın konsolunda çalıştır:

<!-- beforebegin -->
<ul id="list">
  <!-- afterbegin -->
  <li>Existing item</li>
  <!-- beforeend -->
</ul>
<!-- afterend -->
const list = document.querySelector("#list");

list.insertAdjacentHTML("afterbegin", "<li>At the start</li>");
list.insertAdjacentHTML("beforeend", "<li>At the end</li>");
list.insertAdjacentHTML("beforebegin", "<h3>List heading</h3>");
list.insertAdjacentHTML("afterend", "<p>Below the list</p>");

beforebegin ve afterend, element'in dışına, yani kardeşi olarak ekler. afterbegin ve beforeend ise içine, ilk ya da son çocuk olarak.

Neden list.innerHTML += "<li>...</li>" değil? Çünkü += masum görünse de mevcut tüm içeriği string'e çevirir, yenisini ekler ve her şeyi baştan oluşturur. Eski element'lere bağlı event listener'lar kaybolur, input'lara yazılmış değerler silinir. insertAdjacentHTML mevcut node'lara dokunmaz.

Güvenlik uyarısı burada da geçerli: kullanıcıdan ya da sunucudan gelen veriyi asla bu fonksiyonlara HTML olarak verme. Metin eklemek istiyorsan insertAdjacentText, hazır bir element eklemek istiyorsan insertAdjacentElement ya da daha okunur kardeşleri olan before(), after(), prepend() ve append() var.

DocumentFragment, template ve cloneNode

Bir listeye yüz element eklerken her birini ayrı ayrı sayfaya koymak, tarayıcıyı yüz kez "sayfa değişti" diye uyarmak demek. Bunun yerine element'leri önce bir tepsiye dizip tepsiyi tek seferde masaya koyabilirsin. O tepsinin adı DocumentFragment: sayfaya bağlı olmayan, hafif bir node kabı. Sayfaya eklendiğinde kendisi kaybolur, içindekiler yerine geçer.

HTML'deki template etiketi de buna eşlik eder. İçine yazdığın HTML ayrıştırılır ama ekranda görünmez, script'leri çalışmaz, görselleri yüklenmez. Bir kalıp gibi bekler. Aşağıdaki dosyayı kaydedip aç:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>template and fragment</title>
</head>
<body>
  <ul id="products"></ul>

  <template id="product-template">
    <li class="product">
      <strong class="name"></strong>
      <span class="price"></span>
    </li>
  </template>

  <script>
    const products = [
      { id: 1, name: "Headphones", price: 59 },
      { id: 2, name: "Keyboard", price: 110 },
      { id: 3, name: "Mouse", price: 39 },
    ];

    const template = document.querySelector("#product-template");
    const fragment = document.createDocumentFragment();

    for (const product of products) {
      const item = template.content.cloneNode(true);
      const li = item.querySelector(".product");
      li.dataset.id = product.id;
      item.querySelector(".name").textContent = product.name;
      item.querySelector(".price").textContent = ` $${product.price}`;
      fragment.append(item);
    }

    document.querySelector("#products").append(fragment);
  </script>
</body>
</html>

template.content, template'in içindekileri tutan bir DocumentFragment'tir. Her ürün için onu cloneNode(true) ile kopyalıyor, içini textContent ile güvenli şekilde dolduruyor ve fragment'a ekliyoruz. Sayfaya dokunuş en sonda, tek bir append ile oluyor. HTML yapısı HTML'de, mantık JavaScript'te kalıyor; string birleştirerek HTML üretmekten çok daha temiz.

cloneNode'un birkaç inceliği var. Parametre true olursa tüm alt ağaç kopyalanır; verilmezse varsayılan false'tur ve sadece element'in kendisi, içi boş olarak kopyalanır. Attribute'lar kopyalanır ama addEventListener ile eklenmiş listener'lar kopyalanmaz. Bir de kopyaladığın element'in id'si varsa, aynı sayfada iki aynı id oluşur; kopyayı sayfaya eklemeden önce onu değiştir.

Element'i Ölçmek: getBoundingClientRect

Bir tooltip'i butonun hemen altına konumlandırmak ya da bir element'in ekranda görünüp görünmediğini anlamak için onun nerede ve ne büyüklükte olduğunu bilmen gerekir. getBoundingClientRect() bunu söyler. Bunu template örneğindeki sayfanın konsolunda dene:

const box = document.querySelector("#products");
const rect = box.getBoundingClientRect();

console.log(rect.width, rect.height); // piksel cinsinden boyut
console.log(rect.top, rect.left);     // görünür alanın sol üst köşesine göre konum

const pageTop = rect.top + window.scrollY; // sayfanın en üstüne göre konum

const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
console.log(isVisible);

Dönen değerler görünür alana, yani viewport'a göredir. Sayfayı kaydırdıkça top değişir. Sayfanın en başına göre konumu istiyorsan window.scrollY'yi eklersin. Kaydırma sırasında görünürlüğü sürekli takip etmenin daha verimli yolunu İleri seride Observer API'leri ile göreceğiz.

Reflow, Repaint ve Toplu Güncelleme

DOM'u değiştirdiğinde tarayıcı ekranı hemen yeniden çizmez. Önce stilleri hesaplar, sonra her element'in nerede ve ne büyüklükte olduğunu hesaplar (buna layout ya da reflow denir), en son da pikselleri boyar (repaint). Bir yazının rengini değiştirmek genelde sadece yeniden boyamayı gerektirir. Ama genişliği değiştirmek, yeni element eklemek ya da yazı boyutunu büyütmek, etrafındaki her şeyin yerini etkileyebileceği için reflow gerektirir ve daha pahalıdır.

Tarayıcı akıllıdır: yaptığın değişiklikleri biriktirir ve bir sonraki çizimde topluca işler. Bu biriktirmeyi bozan şey, yazdıktan hemen sonra ölçmektir. offsetHeight ya da getBoundingClientRect() gibi bir ölçüm istediğinde tarayıcı sana doğru cevabı verebilmek için bekleyen tüm değişiklikleri o an hesaplamak zorunda kalır. Bunu bir döngü içinde yaparsan her turda zorunlu bir reflow tetiklersin. Yine template örneğindeki sayfada:

const items = document.querySelectorAll(".product");

// Kötü: her turda yaz, sonra oku. Her okuma bir reflow tetikler.
for (const item of items) {
  item.style.height = "40px";
  console.log(item.offsetHeight);
}

// İyi: önce hepsini oku, sonra hepsini yaz.
const heights = [...items].map((item) => item.offsetHeight);
items.forEach((item, i) => {
  item.style.height = `${heights[i] + 10}px`;
});

Toplu güncelleme için birkaç pratik alışkanlık var. Çok sayıda element ekleyeceksen bir fragment'ta topla ve tek seferde ekle. Bir listeyi tamamen yenileyeceksen replaceChildren() ile eskisini tek hamlede değiştir. Bir element'in birkaç stilini birden değiştireceksen style'a tek tek dokunmak yerine CSS'te bir class tanımla ve classList.add ile onu ekle. Okumaları ve yazmaları birbirinden ayır. Bu konunun derinine, yani layout thrashing'e ve DevTools'un Performance sekmesiyle ölçüm yapmaya İleri seride gireceğiz.

Sık Düşülen Tuzaklar

Birincisi, children ile childNodes'u, nextElementSibling ile nextSibling'i karıştırmak. Adında "Element" geçmeyen versiyonlar metin node'larını da sayar ve sana bir boşluk karakteri döndürebilir.

İkincisi, dataset değerlerini sayı ya da boolean sanmak. Baştaki soruda gördüğümüz gibi her şey string'dir; dönüştürmeyi kendin yap.

Üçüncüsü, innerHTML += ile ekleme yapmak. Mevcut tüm içerik baştan oluşturulur, listener'lar ve form değerleri gider. Ekleme için insertAdjacentHTML, append ya da bir fragment kullan.

Dördüncüsü, template'in kendisini kopyalamak. template.cloneNode(true) sana yine görünmeyen bir template verir; kopyalaman gereken template.content'tir.

Beşincisi, closest ya da querySelector'ın null dönebileceğini unutmak. Sonucu bir sonraki satırda kullanmadan önce kontrol et ya da daha önce gördüğümüz ?. operatörünü kullan.

Küçük Bir Deneme

★☆☆ Gezinme örneğindeki menüde, .active class'ını bir sonraki li'ye taşıyan bir kod yaz. Son element'teyken ilk element'e dönsün. İpucu: nextElementSibling null dönerse parentElement.firstElementChild'a git.

★★☆ Çıktıyı tahmin et, sonra dene:

const div = document.createElement("div");
div.dataset.count = 5;
div.dataset.count += 1;
div.dataset.userName = "Ada";

console.log(div.dataset.count);
console.log(div.outerHTML);

★★★ template örneğini, veriyi bir önceki yazıdaki gibi fetch ile https://jsonplaceholder.typicode.com/users adresinden çekecek şekilde değiştir. Template'i her kullanıcının adını ve şehrini (address.city) gösterecek biçimde düzenle, kartları tek bir fragment ile sayfaya ekle ve veri gelene kadar listenin yerinde "Loading..." yazsın.

Bu yazıda element'leri bulduk, ölçtük, kopyaladık ve topluca ekledik. Ama bir listeye yüz kart eklediğinde, her birinin "Delete" butonuna ayrı ayrı listener bağlamak da ayrı bir yük. Bir sonraki yazıda event'lerin DOM ağacında nasıl yolculuk ettiğini, bubbling ve capturing'i, tek bir listener ile yüzlerce element'i yöneten event delegation'ı ve kendi event'lerimizi üretmeyi göreceğiz.