Blok 3 · Ders 22
DOM: JavaScript sayfayla konuşuyor
Sayfadaki öğeleri JavaScript ile bulmak, değiştirmek, oluşturmak ve tıklama gibi olaylara tepki vermek.
Bu dersin sonunda
- Bir HTML sayfasının DOM ağacını okuyup
querySelectorvequerySelectorAllile istediğin öğeyi bulabilirsin <script>etiketinideferile doğru yere koyabilir,nullhatasının sebebini açıklayabilirsin- Yazıyı
textContentile güvenle değiştirebilir,innerHTML'in XSS tehlikesini bir örnekle gösterebilirsin classList,style,createElement,appendveremoveile sayfanın görünümünü ve yapısını değiştirebilirsinaddEventListenerile tıklamaya, yazmaya ve tuşlara cevap verip olay nesnesini kullanabilirsin- Olay yetkilendirmesiyle tek dinleyicide çalışan bir TFT dükkânı arayüzü yapabilirsin
Bu sayfada
- Sayfanın canlı hâli: DOM
- İlk betik: Console artık tarayıcıda
- Öğe bulmak: querySelector
- defer: betik ne zaman çalışır?
- İçeriği değiştirmek: textContent ve innerHTML
- Görünümü değiştirmek: classList ve style
- Öğe oluşturmak: createElement, append, remove
- Olaylar: sayfa sana haber verir
- Olay yetkilendirme: tek dinleyici, çok öğe
- Uygulama: TFT dükkânı
- Sık hatalar
- Görevler
- Kendini sına
- Kayıt noktası
Şimdiye kadar iki ayrı dünyada çalıştın. JavaScript'i terminalde Node ile çalıştırdın; HTML ve CSS ile de güzel ama kıpırdamayan sayfalar yaptın: düğmeye basınca hiçbir şey olmuyordu. Bu derste ikisini birleştiriyorsun. JavaScript sayfadaki öğeleri bulacak, yazılarını değiştirecek, yeni öğeler ekleyecek ve tıklamalara, tuşlara cevap verecek.
Dersin sonunda altın toplayıp birim satın aldığın, D tuşuyla yenilenen küçük bir TFT dükkânı yapmış olacaksın. Görevlerde de Ders 21'deki şampiyon kartlarına bir rol filtresi ekleyeceksin: menüde Büyücü'ye basınca yalnız büyücüler kalacak. Yol boyunca bir güvenlik açığı da göreceksin: yanlış yöntemle sayfaya konan bir oyuncu mesajı, başkasının kodunu senin sayfanda çalıştırabiliyor.
Sayfanın canlı hâli: DOM
Bu derste yeni bir klasörde çalışacaksın. Dosya Gezgini'nde C:\kod içinde dom adında bir klasör oluştur. VS Code'da File → Open Folder ile bu klasörü seç ve içinde üç dosya oluştur: index.html, stil.css ve sayac.js. Önce index.html'i yaz:
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Altın Sayacı</title> <link rel="stylesheet" href="stil.css"> <script src="sayac.js" defer></script></head><body> <h1>Altın Sayacı</h1> <p class="altin">Altın: 0</p> <button id="topla">Altın topla</button> <ul class="takim"> <li>Ahri</li> <li>Garen</li> <li>Jinx</li> </ul></body></html><script> satırındaki defer sözcüğünü (8. satır) şimdilik olduğu gibi yaz. Ne işe yaradığını birazdan onu silip sayfayı bozarak göreceksin.
stil.css'e yalnızca şunu yaz; sayac.js şimdilik boş kalsın:
body { font-family: "Segoe UI", Arial, sans-serif; margin: 24px;}Dosyaları kaydet ve VS Code'un sağ altındaki Go Live'a bas. Sayfa http://127.0.0.1:5500/index.html adresinde açılır: bir başlık, Altın: 0 yazısı, bir düğme ve üç şampiyonluk bir liste.
Tarayıcı bu dosyayı ekrana harf harf basmaz. Önce her etiketten bir obje üretir ve bu objeleri, HTML'deki iç içe yazılışa göre birbirine bağlar. Ortaya bir ağaç çıkar:
index.html'in DOM ağacı. HTML'de iç içe yazdığın her etiket, ağaçta bir dal.Ağaçtaki her parçaya düğüm (node) denir. Etiketlerden öğe düğümleri, etiketlerin içindeki yazılardan yazı düğümleri oluşur. Bir öğenin içindekiler onun çocukları, onu içine alanlar da atalarıdır: üç li'nin atası ul, onun atası body, en tepedeki ata da document.
Bunu kendi gözünle gör. Tarayıcıda F12'ye bas ve Console sekmesine geç (Türkçe tarayıcıda Konsol). En alttaki > işaretinin yanına document.title yazıp Enter'a bas, sonra ikinci satırı yaz:
> document.title< 'Altın Sayacı'> document.title = "Pentakill!"< 'Pentakill!'> ile başlayan satırlar senin yazdıkların, < ile başlayanlar Console'un cevabı. document, ağacın kökündeki obje; Ders 4'teki objeler gibi alanlarını noktayla okursun. İkinci satırdan sonra sekmedeki sayfa adı Pentakill! olur: DOM'u değiştirdin. Şimdi F5'e bas ve document.title'ı yeniden sor: cevap yine 'Altın Sayacı'. Dosyan hiç değişmedi; değişen, maçın canlı hâliydi.
İlk betik: Console artık tarayıcıda
Console tek satırlık denemeler için güzel, ama asıl kod dosyada durur. Ders 18'de uygulama.js'in yazdıklarını Console'da görmüştün; bu kez ikinci satır bilgiyi sayfanın kendisinden, DOM'dan okuyacak. sayac.js'e şu iki satırı yaz ve kaydet:
console.log("Merhaba, tarayıcı")console.log(document.title)Bu sefer çıktı VS Code'un terminaline gelmez. Tarayıcıda F12 ile Console'u aç:
Merhaba, tarayıcıAltın SayacıLive Server, dosyayı kaydettiğin anda sayfayı kendisi yeniler. Her satırın sağında onu yazdıran dosya ve satır numarası (sayac.js:1) görünür; üstüne tıklarsan kod açılır.
Console'da bu derste göstermediğimiz iki satır daha görebilirsin; ikisini de Ders 18'den tanıyorsun. Biri Live reload enabled.: Live Server'ın kendi mesajı, sayfa yenilenince genellikle bir daha çıkmaz. Öteki kırmızı GET http://127.0.0.1:5500/favicon.ico 404 (Not Found): tarayıcı sekmedeki küçük simgeyi istedi, sende öyle bir dosya yok. Zararsız, geçebilirsin.
Bu derste sayac.js'i her bölümde baştan yazacaksın: önceki satırları sil ya da başlarına // koy. Alışkanlıkla terminalde node sayac.js yazarsan şu hatayı alırsın:
ReferenceError: document is not definedNe zaman çıkar? Tarayıcı için yazılmış bir dosyayı terminalde node ile çalıştırınca. document yalnızca tarayıcıda var; Node'un önünde bir sayfa yok.
console.log("Merhaba, tarayıcı")console.log(document.title)Merhaba, tarayıcıC:\kod\dom\sayac.js:2console.log(document.title) ^ ReferenceError: document is not defined at Object.<anonymous> (C:\kod\dom\sayac.js:2:13) ... Node.js v24.20.0Nasıl düzelir? Sayfa kodunu tarayıcıda çalıştır: Live Server'la sayfayı aç, çıktıya Console'dan bak. İlk satırın çalıştığına dikkat et; Node, document'e gelene kadar sorun görmedi.
Öğe bulmak: querySelector
Sayfadaki bir şeyi değiştirmek için önce onu ağaçta bulman gerekir. Bunu Ders 20'de CSS'te kullandığın seçicilerin aynısıyla yaparsın:
const baslik = document.querySelector("h1")const altinYazisi = document.querySelector(".altin")const dugme = document.querySelector("#topla")const ilkSampiyon = document.querySelector(".takim li") console.log(baslik.textContent)console.log(altinYazisi.textContent)console.log(dugme.textContent)console.log(ilkSampiyon.textContent)Altın SayacıAltın: 0Altın toplaAhridocument.querySelector(seçici) ağacı yukarıdan aşağı tarar ve seçiciye uyan ilk öğeyi verir. textContent öğenin içindeki yazıdır; birazdan onu değiştireceksin.
| Seçici | Neyi bulur | Bu sayfada |
|---|---|---|
"h1" | etiket adına göre | ilk h1 |
".altin" | class niteliğinde altin olan | Altın: 0 paragrafı |
"#topla" | id'si topla olan | düğme |
".takim li" | .takim'ın içindeki li | ilki: Ahri |
Seçiciye uyan öğe yoksa querySelector hata vermez, null verir (Ders 12'de gördüğün "burada bir şey yok" değeri):
const mana = document.querySelector(".mana")console.log(mana)nullBu sessizlik tehlikeli: hata, null'ı kullanmaya kalktığın satırda patlar. Dersin en sık hatası bu; iki ayrı sebebini aşağıda göreceksin.
Hepsini bulmak: querySelectorAll
Uyan öğelerin hepsini istiyorsan querySelectorAll:
const sampiyonlar = document.querySelectorAll(".takim li")console.log(sampiyonlar.length) sampiyonlar.forEach((li) => { console.log(li.textContent)})3AhriGarenJinxDönen şey dizi değil, NodeList (düğüm listesi). Diziye benzer: length'i var, forEach ve for...of ile gezilir. console.log(sampiyonlar) yazarsan Console'da NodeList(3) [li, li, li] görürsün. Ama dizi metotlarının çoğu onda yok:
const sampiyonlar = document.querySelectorAll(".takim li")const adlar = sampiyonlar.map((li) => li.textContent)Uncaught TypeError: sampiyonlar.map is not a function at sayac.js:2:27map gerekiyorsa NodeList'i önce Ders 12'deki yaymayla gerçek bir diziye çevir. Aşağıdaki join(", ")'i Ders 17'den hatırla: dizinin elemanlarını araya virgül koyarak tek bir metinde birleştirir.
const sampiyonlar = document.querySelectorAll(".takim li")const adlar = [...sampiyonlar].map((li) => li.textContent)console.log(adlar.join(", "))Ahri, Garen, Jinxİki not daha. querySelector yalnız document'te değil, her öğede çalışır; o zaman yalnız o öğenin içinde arar: takim.querySelectorAll("li"). Dükkânda bunu çok kullanacağız. İnternetteki kodlarda document.getElementById("topla") da görürsün; querySelector("#topla") ile aynı işi yapar, tek farkı başına # konmaması. Bu kursta hep querySelector kullanıyoruz: tek yöntem, CSS'ten bildiğin bütün seçiciler.
Uncaught TypeError: Cannot set properties of null (setting 'textContent')Ne zaman çıkar? querySelector öğeyi bulamayıp null verdiğinde o null'a bir şey yazmaya çalışınca. En sık sebep seçicide noktayı ya da #'yi unutmak:
const altinYazisi = document.querySelector("altin")altinYazisi.textContent = "Altın: 300"Uncaught TypeError: Cannot set properties of null (setting 'textContent') at sayac.js:2:25Nasıl düzelir? "altin" seçicisi <altin> adında bir etiket arar; öyle bir etiket yok. Sınıf için ".altin", kimlik için "#topla" yaz. Emin olamazsan Console'a document.querySelector(".altin") yazıp Enter'a bas: null görüyorsan seçici yanlıştır. Değeri okurken aynı hatanın öbür hâlini alırsın: Cannot read properties of null (reading 'textContent').
defer: betik ne zaman çalışır?
Şimdi söz verdiğimiz bozmayı yapalım. sayac.js'e şu iki satırı yaz, sonra index.html'in 8. satırından defer'i silip kaydet:
const baslik = document.querySelector("h1")console.log(baslik.textContent) <script src="sayac.js"></script>Uncaught TypeError: Cannot read properties of null (reading 'textContent') at sayac.js:2:20Seçici doğru, h1 sayfada duruyor. Neden null? Tarayıcı HTML'i yukarıdan aşağı okur. defer'siz bir <script> görünce okumayı durdurur, betiği indirir ve hemen çalıştırır. O anda hâlâ head'in içindeyiz: body daha okunmadı, h1 ağaçta henüz yok.
sayac.js, üç ayrı yerde. Sorun betiğin kendisinde değil, ne zaman çalıştığında.İki çözüm var:
<script>'ibody'nin en sonuna koymak:</body>'den hemen önce. Betik çalıştığında sayfa okunmuş olur. Eski sitelerde sık görürsün.head'de bırakıpdefereklemek: "betiği indir ama HTML'in tamamı okunana kadar çalıştırma" demek. Tarayıcı bu arada sayfayı okumayı sürdürür, birden çokdefer'li betik de yazıldıkları sırayla çalışır. Bu kursta hep bunu kullanıyoruz.
defer'i geri koy ve kaydet; Console'da Altın Sayacı yazar, hata kaybolur.
İçeriği değiştirmek: textContent ve innerHTML
Öğeyi bulmak işin yarısı. textContent'i okuduğun gibi ona yazabilirsin de:
let altin = 300const altinYazisi = document.querySelector(".altin") console.log(altinYazisi.textContent)altinYazisi.textContent = `Altın: ${altin}`console.log(altinYazisi.textContent)Altın: 0Altın: 300Sayfada da Altın: 300 yazar. Değişkeni yazının içine Ders 9'daki şablon metinle koyduk. textContent'e ne verirsen, harfi harfine yazı olarak gider.
Bir de innerHTML var. O da öğenin içini değiştirir ama verdiğin metni HTML olarak okur: içindeki etiketlerden gerçek öğeler kurar. Farkı görmek için index.html'de </ul>'nin altına boş bir paragraf ekle:
<p class="mesaj"></p>const haber = "<strong>Ahri</strong> çift öldürme yaptı!"const mesajKutusu = document.querySelector(".mesaj") mesajKutusu.textContent = haberSonra son satırı mesajKutusu.innerHTML = haber yap ve farka bak:

textContent: etiketler harf olarak görünüyor.
innerHTML: <strong> gerçek bir öğe oldu.innerHTML'in tehlikesi: XSS
Bu gücün bir bedeli var. Diyelim ki sayfan başka oyuncuların mesajlarını gösteriyor: sohbet, yorum, oyuncu adı. O metni sen yazmadın, bir kullanıcı yazdı. Kötü niyetli biri mesaj olarak şunu gönderirse ne olur?
const gelenMesaj = `<img src="yok.png" onerror="document.body.textContent='Bu sayfayı artık saldırgan yönetiyor'">`const mesajKutusu = document.querySelector(".mesaj") mesajKutusu.innerHTML = gelenMesajinnerHTML bu metinden gerçek bir <img> öğesi kurar. yok.png diye bir resim olmadığı için resim yüklenemez ve tarayıcı onerror niteliğindeki kodu çalıştırır. Saldırganın kodu artık senin sayfanda, senin ziyaretçinin tarayıcısında çalışıyor. Sonra son satırı mesajKutusu.textContent = gelenMesaj yapıp kaydet ve iki sonucu karşılaştır:

innerHTML ile: sayfanın tamamı gitti.
textContent ile: yalnızca yazı.Console'a bakarsan kırmızı bir GET http://127.0.0.1:5500/yok.png 404 (Not Found) satırı da görürsün: tarayıcı yok.png'yi gerçekten istedi, çünkü innerHTML gerçek bir resim öğesi kurmuştu. (Geliştirici araçlarını sayfa açıldıktan sonra açtıysan aynı satır Failed to load resource: the server responded with a status of 404 (Not Found) diye görünür, sağında yok.png:1 yazar.)
Bu örnek yalnızca sayfayı sildi. Gerçek bir saldırgan aynı yere ziyaretçiden şifresini isteyen sahte bir giriş kutusu çizebilir ya da onun adına mesaj gönderebilir. Bu saldırının adı XSS.
textContent bir yazı düğümü koyar; innerHTML metinden öğe kurar.Bu yüzden üç kuralı aklında tut:
- Kullanıcıdan, başka bir oyuncudan ya da internetten gelen metni asla
innerHTML'e verme. Yazı için heptextContent. - Yeni bir öğe gerekiyorsa birazdan göreceğin
createElement'i kullan, içine yazıyı yinetextContentile koy. innerHTML'i yalnızca tamamen kendi yazdığın, sabit HTML için kullan. Emin değilsen kullanma.
Görünümü değiştirmek: classList ve style
Görünüm CSS'in işi. JavaScript'in görevi, doğru anda doğru sınıfı takıp çıkarmak. stil.css'e şu kuralı ekle:
.zengin { color: darkgoldenrod; font-weight: bold;}Bir öğenin sınıflarını classList ile yönetirsin:
const altinYazisi = document.querySelector(".altin") altinYazisi.classList.add("zengin")console.log(altinYazisi.classList.contains("zengin")) altinYazisi.classList.remove("zengin")console.log(altinYazisi.classList.contains("zengin")) altinYazisi.classList.toggle("zengin")console.log(altinYazisi.classList.contains("zengin"))console.log(altinYazisi.className)truefalsetruealtin zengin| Metot | Ne yapar |
|---|---|
add("zengin") | sınıfı ekler; zaten varsa bir şey yapmaz |
remove("zengin") | sınıfı çıkarır; yoksa bir şey yapmaz |
toggle("zengin") | varsa çıkarır, yoksa ekler |
contains("zengin") | sınıf var mı: true ya da false |
Son satırdaki className, öğenin bütün sınıflarını tek bir metin olarak verir. add'in eski sınıflara dokunmadığına dikkat et: altin yerinde duruyor, zengin yanına eklendi. Sınıf adını noktasız yaz: classList.add(".zengin") adında nokta olan bir sınıf ekler ve CSS'teki hiçbir kural ona uymaz.
toggle'a ikinci değer olarak bir koşul verirsen ne yapacağına koşul karar verir: true ise ekler, false ise çıkarır. "Altın 100 ya da üstündeyse altın rengine boya" kuralı tek satır olur:
const altinYazisi = document.querySelector(".altin")let altin = 120 altinYazisi.textContent = `Altın: ${altin}`altinYazisi.classList.toggle("zengin", altin >= 100)console.log(altinYazisi.className)altin zenginstyle: hesaplanan değerler için
Bazen değer önceden belli değildir: can barının yüzde kaçı dolu? Böyle hesaplanan değerler için style kullanılır. index.html'de mesaj paragrafının altına bir can barı, stil.css'e de görünümünü ekle:
<div class="can"> <div class="can-dolu"></div> </div>.can { width: 240px; height: 16px; background: #ddd; border-radius: 8px;}.can-dolu { width: 100%; height: 100%; background: seagreen; border-radius: 8px;}const canBari = document.querySelector(".can-dolu")let hp = 70 canBari.style.width = `${hp}%`if (hp < 30) { canBari.style.backgroundColor = "crimson"}Kaydet ve bara bak; sonra hp'yi 25 yapıp yeniden kaydet:

hp = 70: barın %70'i dolu.
hp = 25: 30'un altında, bar kırmızı.style kullanırken üç kural:
- CSS'teki tireli adlar JavaScript'te camelCase olur:
background-color→backgroundColor,font-size→fontSize. - Değeri CSS'te yazdığın gibi, birimiyle birlikte bir metin olarak ver:
"70%","120px".canBari.style.width = 70yazarsan tarayıcı satırı sessizce yok sayar: bar değişmez, hata da çıkmaz. styleyalnızca öğenin kendistyleniteliğine yazar ve oradan okur. Ders 20'den bildiğin gibistyleniteliği bütün seçicileri yener;crimsonbu yüzdenstil.css'tekiseagreen'in önüne geçti. Tersi olmaz:seagreen'icanBari.style.backgroundColorile okuyamazsın, boş metin gelir. Kalıcı görünümleri (renk, gizle/göster) sınıfla yap;style'ı hesaplanan değerlere bırak.
Uncaught SyntaxError: Invalid left-hand side in assignmentNe zaman çıkar? CSS özelliğini JavaScript'te tireli adıyla yazınca:
const canBari = document.querySelector(".can-dolu")canBari.style.background-color = "crimson"Uncaught SyntaxError: Invalid left-hand side in assignment (at sayac.js:2:1)Sondaki (at sayac.js:2:1) yeri söyler: 2. satır, 1. sütun. SyntaxError'da altta at … satırı olmaz, çünkü kod hiç çalışmaya başlamadı.
Nasıl düzelir? JavaScript tireyi eksi işareti sanar ve satırı canBari.style.background - color = "crimson" diye okur: "bir çıkarma işleminin sonucuna değer koy". Bu anlamsız olduğu için ='in sol tarafını geçersiz bulur. Adı camelCase yaz: backgroundColor. Bu bir SyntaxError olduğu için, Ders 1'deki gibi dosyanın hiçbir satırı çalışmaz.
Öğe oluşturmak: createElement, append, remove
Takıma yeni bir şampiyon katmak, dükkâna yeni kart koymak için sayfada olmayan bir öğeyi sıfırdan yaratman gerekir:
const takim = document.querySelector(".takim") const yeni = document.createElement("li")yeni.textContent = "Lux"console.log(yeni.outerHTML) takim.append(yeni)console.log(takim.querySelectorAll("li").length)<li>Lux</li>4Üç adım var. document.createElement("li") yeni bir li öğesi yaratır ama onu sayfaya koymaz; öğe bellekte bekler. textContent içini doldurur (outerHTML, öğenin kendisiyle birlikte HTML hâlidir; burada yalnızca bakmak için yazdırdık). append ise öğeyi ul'nin son çocuğu olarak ağaca takar ve Lux ekranda belirir.
Bir diziden liste kurmak da aynı kalıp, yalnızca döngünün içinde. Öğe silmek için remove() yeter:
const takim = document.querySelector(".takim")const yedekler = ["Teemo", "Annie", "Yasuo"] yedekler.forEach((ad) => { const li = document.createElement("li") li.textContent = ad takim.append(li)}) const ilk = document.querySelector(".takim li")ilk.remove()console.log(takim.querySelectorAll("li").length)5Liste artık Garen, Jinx, Teemo, Annie, Yasuo: üç öğe eklendi, Ahri remove() ile ağaçtan söküldü. Bilmen gereken birkaç şey daha:
appendhep sona ekler; başa eklemek içinprepend.appendbirden çok şeyi ve düz yazıyı da alır:kart.append(adYazisi, " 3 altın").- Ağaçta zaten duran bir öğeyi başka yere
appendedersen kopyası çıkmaz, öğe taşınır. Dükkânda satın alınan kartı yedek kulübesine böyle taşıyacağız. - Bir öğenin bütün çocuklarını silmek için
takim.replaceChildren(). - Eski kodlarda
appendChildgörürsün; yalnızca tek öğe alır.appendonun daha esnek hâli.
Öğeye bilgi iliştirmek: data-*
Dükkânda her kartın fiyatını bilmemiz gerekecek. Bir öğeye kendi bilgini data- ile başlayan bir nitelikle iliştirebilirsin; JavaScript'te dataset ile okunur ve yazılır:
const kart = document.createElement("li")kart.textContent = "Jinx"kart.dataset.maliyet = 3 console.log(kart.outerHTML)console.log(kart.dataset.maliyet + 1)console.log(Number(kart.dataset.maliyet) + 1)<li data-maliyet="3">Jinx</li>314dataset.maliyet'e sayı versen de HTML niteliği olarak metin saklanır. Ders 1'deki "5" + 3 tuzağı burada da bekliyor: hesap yapmadan önce Number(...) ile sayıya çevir (Ders 9).
Olaylar: sayfa sana haber verir
Şu ana kadarki kodların hepsi sayfa açılırken bir kez çalıştı ve bitti. Oyun öyle değil: oyuncu tıklar, tuşa basar, yazar; sayfa da o anda cevap vermeli. Kullanıcının yaptığı her şey tarayıcıda bir olaydır ve addEventListener ile bir öğeye "şu olay olunca bu fonksiyonu çalıştır" dersin.
İşte altın sayacı. sayac.js'i baştan yaz:
let altin = 0const altinYazisi = document.querySelector(".altin")const dugme = document.querySelector("#topla") function altinEkle() { altin = altin + 1 altinYazisi.textContent = `Altın: ${altin}`} dugme.addEventListener("click", altinEkle)

addEventListener iki şey alır: olayın adı ("click") ve olay olunca çalışacak fonksiyon. Önce değişkeni değiştiriyoruz, sonra ekranı ona göre güncelliyoruz.
Fonksiyonu parantezsiz verdiğimize dikkat et: altinEkle. Bu, Ders 10'daki geri çağırma (callback) fikri: fonksiyonu veriyorsun, çağırma işini tarayıcı tıklama anında yapacak. Ders 15'teki olay döngüsü burada da iş başında: tıklama, o an çalışan kod bitince sırası gelip işlenir.
altinEkle()Ne zaman çıkar? Dinleyiciye fonksiyonun adı yerine onun çağrısını verince:
dugme.addEventListener("click", altinEkle())Sayfa açılır açılmaz Altın: 1 yazar; sonra düğmeye ne kadar basarsan bas sayı değişmez. Console'da hata yok.
Nasıl düzelir? altinEkle() sayfa yüklenirken bir kez çalışır (altın 1 olur) ve geriye undefined döndürür. Dinleyiciye fonksiyon değil undefined verilmiş olur, tarayıcı da bunu sessizce yok sayar. Parantezleri sil: addEventListener("click", altinEkle). Ders 3'teki tarif benzetmesiyle: tarifi vermen gerekirken yemeği kendin pişirip tabağı uzattın.
Fonksiyonu ayrıca tanımlamak yerine dinleyicinin içine bir ok fonksiyonu da yazabilirsin; çoğu kodda böyle görürsün. Sayaç aynı çalışır:
let altin = 0const altinYazisi = document.querySelector(".altin")const dugme = document.querySelector("#topla") dugme.addEventListener("click", () => { altin = altin + 1 altinYazisi.textContent = `Altın: ${altin}`})Olay nesnesi
Tarayıcı dinleyicini çağırırken ona bir obje verir: olayın ayrıntıları. Fonksiyona bir parametre koyarsan onu yakalarsın. Adını sen seçersin; çoğu kodda e ya da event görürsün, biz olay diyoruz:
const dugme = document.querySelector("#topla") dugme.addEventListener("click", (olay) => { console.log(olay.type) console.log(olay.target.textContent)})clickAltın toplaolay.type olayın adı, olay.target da olayın gerçekleştiği öğe: tıklanan düğme. target, dersin son bölümünde baş rolde olacak.
Yazmayı dinlemek: input
index.html'de can barının altına bir yazı kutusu ve bir selam paragrafı ekle:
<input id="ad" placeholder="Sihirdar adın"> <p class="selam">Merhaba, sihirdar!</p>const adKutusu = document.querySelector("#ad")const selam = document.querySelector(".selam") adKutusu.addEventListener("input", () => { console.log(adKutusu.value) selam.textContent = `Merhaba, ${adKutusu.value}!`})AAhAhrAhriinput olayı kutunun içeriği her değiştiğinde çalışır: her harfte, silmede, yapıştırmada. Kutudaki yazıyı .value ile okursun; sayfada da harf harf Merhaba, Ahri! belirir. Selamı textContent ile yazdığımıza dikkat et: kutuya HTML yazan biri olsa bile yalnızca yazı olarak görünür. Form kutularını Ders 23'te ayrıntılı göreceksin.
input'u textContent ile okumakNe zaman çıkar? Bir input'un içindekini textContent ile okumaya çalışınca:
adKutusu.addEventListener("input", () => { selam.textContent = `Merhaba, ${adKutusu.textContent}!`})Kutuya ne yazarsan yaz, sayfada Merhaba, ! yazar. Hata mesajı yok.
Nasıl düzelir? input kapanış etiketi olmayan bir öğe; içinde yazı düğümü yoktur, textContent'i hep boş metindir. Kullanıcının yazdığı .value'da durur: adKutusu.value.
Tuşları dinlemek: keydown
document.addEventListener("keydown", (olay) => { console.log(olay.key)})dShiftDfEnterArrowUpEscapeDinleyiciyi document'e taktık; sayfanın neresinde olursan ol, tuşları yakalar. olay.key basılan tuşun adı. Harflerde büyük-küçük farkı var (d ve D) ve Shift'in kendisi de bir tuş. TFT'de D dükkânı yeniler; aynısını yapalım:
document.addEventListener("keydown", (olay) => { if (olay.key === "d" || olay.key === "D") { console.log("Dükkân yenilendi") }})İki hâli birden denetlemezsen, Caps Lock'u açık unutan oyuncu tuşun bozuk olduğunu sanır.
Varsayılan davranışı durdurmak: preventDefault
Bazı öğelerin tarayıcıda zaten bir işi var: bağlantıya tıklayınca başka sayfaya gidilir, formdaki düğmeye basınca sayfa yenilenir. Buna varsayılan davranış denir. Kendi işini yapmak istediğinde, olay nesnesinin preventDefault() metoduyla onu durdurursun. Selam paragrafının altına bir bağlantı ve bir kurallar paragrafı ekle, stil.css'e de .gizli kuralını yaz:
<a href="kurallar.html" id="kurallar-link">Kurallar</a> <p class="kurallar gizli">Altın biriktir, faizini al, dükkânı akıllıca yenile.</p>.gizli { display: none;}const kurallarLink = document.querySelector("#kurallar-link")const kurallar = document.querySelector(".kurallar") kurallarLink.addEventListener("click", (olay) => { olay.preventDefault() kurallar.classList.toggle("gizli")})Bağlantıya tıklayınca artık kurallar.html'e gidilmez; kurallar paragrafı açılıp kapanır. preventDefault() satırını silersen tıklama seni kurallar.html adresine götürür; öyle bir dosya olmadığı için Live Server'ın Cannot GET /kurallar.html yazan sayfasını görürsün. Formlarda sayfanın yenilenmesini durdurmak için aynı metodu Ders 23'te kullanacaksın.
Olay yetkilendirme: tek dinleyici, çok öğe
Takımdaki şampiyona tıklayınca seçilsin istiyorsun. Önce stil.css'e seçili satırın görünümünü ekle:
.secili { background: gold;}İlk akla gelen, querySelectorAll ile hepsini bulup her birine dinleyici takmak:
const sampiyonlar = document.querySelectorAll(".takim li") sampiyonlar.forEach((li) => { li.addEventListener("click", () => { li.classList.toggle("secili") })}) const yeni = document.createElement("li")yeni.textContent = "Lux"document.querySelector(".takim").append(yeni)Garen'e tıkla: sarı olur. Lux'a tıkla: hiçbir şey olmaz. forEach çalıştığında listede yalnızca üç li vardı; Lux sonradan geldi ve ona kimse totem dikmedi. Dükkânda kartlar her yenilemede silinip yeniden yaratılacak; her seferinde dinleyici takmak hem zahmetli hem hata kaynağı.
Çözüm, olayların bir özelliğinde. Bir öğeye tıkladığında olay o öğede kalmaz, kabarcık gibi yukarı çıkar: önce tıklanan li'ye, sonra ul'ye, sonra body'ye, html'e ve document'e ulaşır. Görmek için üç dinleyici tak (document.body, sayfanın body öğesidir) ve Garen'e tıkla:
document.querySelector(".takim").addEventListener("click", () => { console.log("ul duydu")})document.body.addEventListener("click", () => { console.log("body duydu")})document.addEventListener("click", () => { console.log("document duydu")})ul duydubody duydudocument duyduTek tıklama, üç dinleyici. Garen'e tıkladın ama onun bütün ataları da haber aldı.
li'ye ulaşır, sonra sırayla bütün atalarına: ul, body, html, document. Dinleyici bu yoldaki herhangi bir atada durabilir.Öyleyse dinleyiciyi her li'ye değil, hepsinin atası olan ul'ye tak; hangi li'ye tıklandığını olay.target'tan öğren:
const takim = document.querySelector(".takim") takim.addEventListener("click", (olay) => { const li = olay.target.closest("li") if (!li) { return } li.classList.toggle("secili")}) const yeni = document.createElement("li")yeni.textContent = "Lux"takim.append(yeni)
Artık Lux da seçiliyor, çünkü dinleyici Lux'a değil ul'ye takılı. closest neden gerekli? olay.target tıklanan en içteki öğedir. li'nin içinde başka bir etiket olsaydı (<li><b>Jinx</b> 3 altın</li> gibi) ve oyuncu kalın yazıya tıklasaydı target <b> olurdu. closest("li") tıklanan öğeden başlayıp yukarı doğru çıkar ve seçiciye uyan ilk öğeyi verir; öğenin kendisi uyuyorsa onu. Hiç bulamazsa null döner: ul'nin li olmayan boş bir yerine tıklanınca. O durumda Ders 6'daki erken çıkışla fonksiyondan çıkıyoruz.
Uygulama: TFT dükkânı
Şimdi hepsini bir araya getiriyoruz: altın toplayan bir sayaç, beş kartlık bir dükkân, satın aldığın birimlerin beklediği dokuz yerlik bir yedek kulübesi ve D ile yenileme. Bitince böyle görünecek:

C:\kod\dom içinde dukkan adında bir klasör oluştur ve içine üç dosya koy: index.html, stil.css, dukkan.js. Live Server açıkken adres http://127.0.0.1:5500/dukkan/index.html olur.
Adım 1: iskelet ve görünüm
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>TFT Dükkânı</title> <link rel="stylesheet" href="stil.css"> <script src="dukkan.js" defer></script></head><body> <header class="ust"> <p class="altin">Altın: 10</p> <button id="topla">Altın topla (+1)</button> <button id="yenile">Yenile (D) · 2 altın</button> </header> <h2>Dükkân</h2> <div id="dukkan" class="kartlar"></div> <h2>Yedek kulübesi <span class="sayi">0/9</span></h2> <div id="yedek" class="kartlar"></div> <p class="mesaj"></p></body></html>Dükkân ve yedek kulübesi boş div'ler; kartları JavaScript dolduracak.
body { max-width: 640px; margin: 0 auto; padding: 16px; font-family: "Segoe UI", Arial, sans-serif; background: #141824; color: #e8e6e3;}.ust { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;}.altin { margin: 0 auto 0 0; font-size: 1.5rem; font-weight: bold; color: #f5c542;}button { padding: 8px 12px; border: 0; border-radius: 8px; background: #2c3550; color: inherit; font: inherit; cursor: pointer;}.kartlar { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 8px; min-height: 64px;}.kart { border: 2px solid #8a8f98; background: #1e2436; text-align: left;}.kart b { display: block;}.maliyet-2 { border-color: #2fbf71; }.maliyet-3 { border-color: #3b8cff; }.maliyet-4 { border-color: #b45cff; }.maliyet-5 { border-color: #f5c542; }.pahali { opacity: 0.35;}.mesaj { min-height: 24px; color: #9ecbff;}CSS'in neredeyse tamamı Ders 20 ve Ders 21'den. .altin'daki margin: 0 auto 0 0 yalnız sağ kenar boşluğunu auto yapar: flex satırında kalan bütün yeri o boşluk alır ve iki düğmeyi sağa iter. Tek yeni özellik opacity: öğenin ne kadar görüneceği; 1 tam görünür, 0 tamamen saydam, 0.35 soluk.
İki sınıf grubuna dikkat et: maliyet-2…maliyet-5 kartın kenarını TFT'deki gibi maliyetine göre boyar (1 altınlıklar .kart'taki gri kenarla kalır), pahali da altının yetmediği kartı soluklaştırır. İkisini de JavaScript takacak.
Adım 2: veri, durum ve kart yapmak
dukkan.js'in ilk yarısı:
const birimler = [ { ad: "Garen", maliyet: 1 }, { ad: "Teemo", maliyet: 1 }, { ad: "Annie", maliyet: 2 }, { ad: "Yasuo", maliyet: 2 }, { ad: "Lux", maliyet: 3 }, { ad: "Jinx", maliyet: 3 }, { ad: "Ahri", maliyet: 4 }, { ad: "Aurelion Sol", maliyet: 5 }]const yedekSiniri = 9const yenilemeUcreti = 2 let altin = 10 const altinYazisi = document.querySelector(".altin")const dukkan = document.querySelector("#dukkan")const yedek = document.querySelector("#yedek")const yedekSayisi = document.querySelector(".sayi")const mesaj = document.querySelector(".mesaj") function kartYap(birim) { const kart = document.createElement("button") kart.classList.add("kart", `maliyet-${birim.maliyet}`) kart.dataset.ad = birim.ad kart.dataset.maliyet = birim.maliyet const ad = document.createElement("b") ad.textContent = birim.ad kart.append(ad, `${birim.maliyet} altın`) return kart} function ekraniGuncelle() { altinYazisi.textContent = `Altın: ${altin}` const yedektekiler = yedek.querySelectorAll(".kart").length yedekSayisi.textContent = `${yedektekiler}/${yedekSiniri}` dukkan.querySelectorAll(".kart").forEach((kart) => { kart.classList.toggle("pahali", Number(kart.dataset.maliyet) > altin) })} function dukkaniDoldur() { dukkan.replaceChildren() for (let i = 0; i < 5; i++) { const sira = Math.floor(Math.random() * birimler.length) dukkan.append(kartYap(birimler[sira])) } ekraniGuncelle()}Programın aklında tuttuğu şey, yani durumu, iki yerde duruyor: altin değişkeninde ve kartların kendisinde (hangisi dükkânda, hangisi kulübede, fiyatı ne). Altın yazısı, kulübe sayacı ve soluk kartlar bu durumun ekrandaki yansıması. Bütün kodun tek kuralı şu: önce durumu değiştir, sonra ekraniGuncelle() ile ekranı ona uydur. Fonksiyonların işi:
kartYap, bir birim objesinden kart üretir. Kart birbutton: fareyle olduğu kadar Tab ve Enter ile de seçilebilsin diye (Ders 19'daki erişilebilirlik).classList.addaynı anda iki sınıf ekliyor;datasetkartın adını ve fiyatını taşıyor.ekraniGuncelle, altın yazısını ve kulübe sayacını yeniler, sonra dükkândaki her kartapahalisınıfını koşullutoggleile takar ya da çıkarır.dukkaniDoldur, eski kartlarıreplaceChildren()ile söker ve Ders 6'dakiMath.randomile beş rastgele kart koyar.
Adım 3: olaylar
dukkan.js'in ikinci yarısı; dosyanın sonuna ekle:
function yenile() { if (altin < yenilemeUcreti) { mesaj.textContent = `Yenilemek için ${yenilemeUcreti} altın gerekir.` return } altin = altin - yenilemeUcreti mesaj.textContent = "" dukkaniDoldur()} dukkan.addEventListener("click", (olay) => { const kart = olay.target.closest(".kart") if (!kart) { return } const maliyet = Number(kart.dataset.maliyet) if (maliyet > altin) { mesaj.textContent = "Altının yetmiyor." return } if (yedek.querySelectorAll(".kart").length >= yedekSiniri) { mesaj.textContent = "Yedek kulüben dolu." return } altin = altin - maliyet yedek.append(kart) mesaj.textContent = `${kart.dataset.ad} alındı.` ekraniGuncelle()}) yedek.addEventListener("click", (olay) => { const kart = olay.target.closest(".kart") if (!kart) { return } altin = altin + Number(kart.dataset.maliyet) kart.remove() mesaj.textContent = `${kart.dataset.ad} satıldı.` ekraniGuncelle()}) document.querySelector("#topla").addEventListener("click", () => { altin = altin + 1 ekraniGuncelle()})document.querySelector("#yenile").addEventListener("click", yenile)document.addEventListener("keydown", (olay) => { if (olay.key === "d" || olay.key === "D") { yenile() }}) dukkaniDoldur()Satın alma dinleyicisi tek bir tane ve #dukkan'a takılı; kartlar her yenilemede değişse de çalışır. Kartın içindeki kalın ada tıklanınca olay.target <b> olur, closest(".kart") bizi karta çıkarır. Satın alınan kartı yeniden yaratmıyoruz: yedek.append(kart) onu dükkândan kulübeye taşıyor.
Kulübedeki dinleyici de satışı yapıyor: parayı geri verip kartı remove() ile söküyor. kart ağaçtan söküldükten sonra da bellekte durduğu için hemen ardından kart.dataset.ad'ı okuyabiliyoruz. En alttaki dukkaniDoldur() ilk beş kartı koyar.
Sınırlarda dene
Kaydet ve oyna. Her kuralı bir de tam sınırında dene; bunların hepsini senin için denedik:
- Altın 2'nin altına inince D'ye bas: dükkân değişmemeli,
Yenilemek için 2 altın gerekir.yazmalı. - Soluk bir karta tıkla:
Altının yetmiyor.yazmalı, altın azalmamalı. - Kulübe
9/9doluyken bir kart daha al:Yedek kulüben dolu. - Kartların arasındaki boşluğa tıkla: hiçbir şey olmamalı, Console'da hata çıkmamalı.
- Kulübedeki bir birime tıkla: satılmalı, maliyeti kadar altın geri gelmeli.

Sık hatalar
Derste gördüklerine ek olarak sık karşılaşacağın dört hata daha. Hepsi tarayıcının Console'unda çıkar. Bütün derslerin hataları hata rehberinde toplanıyor.
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')Ne zaman çıkar? Dinleyici takmak istediğin öğe null olunca. İki sebebi var: betik head'de ve defer'siz (öğe henüz yok) ya da seçici yanlış (öyle bir öğe yok).
const dugme = document.querySelector("#topla") dugme.addEventListener("click", () => { console.log("Altın +1")})Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') at sayac.js:3:7Nasıl düzelir? Önce <script> satırında defer var mı bak. Varsa seçiciyi Console'da dene: document.querySelector("#topla") null veriyorsa nokta, # ya da yazım hatası arıyorsun.
Uncaught TypeError: document.querySelectorAll(...).addEventListener is not a functionNe zaman çıkar? Bir öğe listesine (NodeList) doğrudan dinleyici takmaya çalışınca:
document.querySelectorAll(".takim li").addEventListener("click", () => { console.log("tıklandı")})Uncaught TypeError: document.querySelectorAll(...).addEventListener is not a function at sayac.js:1:40Nasıl düzelir? addEventListener tek bir öğenin metodu; listenin değil. Ya forEach ile her öğeye tek tek tak ya da (daha iyisi) ortak atalarına tek dinleyici takıp olay yetkilendirmesi kullan. Listeyi önce bir değişkene koyduysan mesaj o adla gelir: sampiyonlar.addEventListener is not a function.
InvalidCharacterErrorNe zaman çıkar? classList.add'e boşluklu tek bir metin verince:
const altinYazisi = document.querySelector(".altin")altinYazisi.classList.add("zengin parlak")Uncaught InvalidCharacterError: Failed to execute 'add' on 'DOMTokenList': The token provided ('zengin parlak') contains HTML space characters, which are not valid in tokens. at sayac.js:2:23Nasıl düzelir? HTML'de class="zengin parlak" iki sınıf demektir, ama classList.add her sınıfı ayrı bir değer olarak ister: classList.add("zengin", "parlak").
404 (Not Found) ve Refused to execute scriptNe zaman çıkar? <script src="..."> içindeki dosya adı yanlış yazılınca (sayc.js) ya da dosya başka klasördeyse. Sayfa açılır ama kodun hiç çalışmaz, console.log'ların bile. Console'a iki kırmızı satır düşer:
GET http://127.0.0.1:5500/sayc.js net::ERR_ABORTED 404 (Not Found)Refused to execute script from 'http://127.0.0.1:5500/sayc.js' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.İlk satır "sunucu 404 dedi: böyle bir dosya yok" demek; adresin sonunda istenen dosyanın adı var. Geliştirici araçlarını sayfa açıldıktan sonra açtıysan aynı satırı Failed to load resource: the server responded with a status of 404 (Not Found) diye görürsün, sağında sayc.js:1; anlamı aynı. İkinci satır Ders 18'de stil dosyasında gördüğün hatanın kardeşi: Live Server dosya yerine Cannot GET /sayc.js yazan küçük bir HTML sayfası gönderdi, tarayıcı da HTML'i betik diye çalıştırmayı reddetti.
Nasıl düzelir? src'deki adı dosyanın gerçek adıyla harf harf karşılaştır; dosya bir alt klasördeyse yolu da yaz (js/sayac.js). Network (Ağ) sekmesinde kırmızı satıra tıklarsan tarayıcının hangi adresi istediğini görürsün.
Görevler
İlk dört görevi C:\kod\dom klasöründe yap. Her görev için dosyaları oluştur (gorev1.html, gorev1.css ve gorev1.js gibi) ve sayfayı http://127.0.0.1:5500/gorev1.html adresinden aç. HTML'i ve CSS'i görevde veriyoruz; senin işin JavaScript.
Görev 5 Ders 21'in klasöründe, Görev 6 dükkânda geçiyor. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Mana iksiri
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Mana iksiri</title> <link rel="stylesheet" href="gorev1.css"> <script src="gorev1.js" defer></script></head><body> <p class="mana">Mana: 40</p> <div class="bar"> <div class="bar-dolu"></div> </div> <button id="iksir">Mana iksiri iç (+20)</button></body></html>.bar { width: 200px; height: 12px; margin-bottom: 12px; background: #ddd;}.bar-dolu { width: 40%; height: 100%; background: royalblue;}Düğmeye her basışta mana 20 artsın; yazı da mavi bar da güncellensin (mana kaçsa bar yüzde o kadar dolu). Mana 100'ü geçmesin; 100'e ulaşınca yazı Mana: 100 (dolu) olsun. Dört kez basınca sayfada sırayla şunlar görünmeli:
Mana: 60Mana: 80Mana: 100 (dolu)Mana: 100 (dolu)Bar da sırayla %60, %80, %100 ve yine %100 dolu olmalı; hiçbir basışta kutusundan taşmamalı.
İpucu 1
Altın sayacının kalıbı: bir let değişken, querySelector ile üç öğe (yazı, bar, düğme), düğmeye bir click dinleyicisi. Dinleyicinin içinde önce değişkeni değiştir, sonra ekranı.
İpucu 2
80 + 20 tam 100 eder, 100 + 20 ise 120. Toplamadan sonra if (mana >= 100) ile 100'e sabitle ve "(dolu)" yazısını o dalda yaz; öbür dalda yalnızca sayıyı yaz.
İpucu 3
Bar, can barındaki gibi style.width ile dolar ve değer birimli bir metin olmalı: `${mana}%`. Bu satırı if'in dışına, dinleyicinin sonuna koy; iki dalda da çalışsın.
Çözümü göster
let mana = 40const manaYazisi = document.querySelector(".mana")const manaBari = document.querySelector(".bar-dolu")const iksir = document.querySelector("#iksir") iksir.addEventListener("click", () => { mana = mana + 20 if (mana >= 100) { mana = 100 manaYazisi.textContent = "Mana: 100 (dolu)" } else { manaYazisi.textContent = `Mana: ${mana}` } manaBari.style.width = `${mana}%`})Üçüncü basış tam sınırda: 80 + 20 100 eder. Koşulu mana > 100 diye yazsaydın o basışta yazı Mana: 100 olurdu, "(dolu)" ancak dördüncü basışta gelirdi. mana = 100 satırını unutsaydın dördüncü basışta yazı yine doğru görünürdü ama bar 120% olup gri kutusunun dışına taşardı. İkisini de denedik.
Maç notu sayacı
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Maç notu</title> <link rel="stylesheet" href="gorev2.css"> <script src="gorev2.js" defer></script></head><body> <label for="not">Maç notu</label> <input id="not"> <p class="sayac">0/30</p></body></html>.asim { color: crimson; font-weight: bold;}Kutuya her yazışta altındaki sayaç uzunluk/30 göstersin. Not 30 karakteri geçince sayaca asim sınıfı eklensin (kırmızı), 30 ve altına inince çıkarılsın. Beklenen: boşken 0/30; tam 30 karakterde 30/30 ve kırmızı değil; 31'de 31/30 ve kırmızı; bir harf silince yine 30/30 ve kırmızı değil.
İpucu 1
input olayını dinle. Kutudaki yazı notKutusu.value, uzunluğu da Ders 9'daki gibi .length.
İpucu 2
Sınıfı if/else ile add ve remove yaparak da yönetebilirsin, ama koşullu toggle tek satır: sayac.classList.toggle("asim", uzunluk > 30).
Çözümü göster
const notKutusu = document.querySelector("#not")const sayac = document.querySelector(".sayac") notKutusu.addEventListener("input", () => { const uzunluk = notKutusu.value.length sayac.textContent = `${uzunluk}/30` sayac.classList.toggle("asim", uzunluk > 30)})Denedik: 30 karakterde sınıf sayac, 31'de sayac asim, bir harf silince yine sayac; kutuyu tamamen boşaltınca 0/30. Koşulu >= 30 yazsaydın tam 30 karakterlik not da kırmızı olurdu.
Güvenli sohbet
Bir arkadaşın sohbet kutusu yazmış ama kodu XSS'e açık:
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Sohbet</title> <script src="gorev3.js" defer></script></head><body> <ul class="sohbet"></ul> <input id="mesaj" placeholder="Mesajın"> <button id="gonder">Gönder</button></body></html>const sohbet = document.querySelector(".sohbet")const mesajKutusu = document.querySelector("#mesaj")const gonder = document.querySelector("#gonder") gonder.addEventListener("click", () => { sohbet.innerHTML = sohbet.innerHTML + `<li>${mesajKutusu.value}</li>` mesajKutusu.value = ""})Önce açığı kendin gör: kutuya <img src="x" onerror="document.body.style.background='red'"> yazıp Gönder'e bas; sayfanın arka planı kırmızı olur. Sonra kodu düzelt:
innerHTMLkullanma. Her mesaj içincreateElementile birliyarat, yazıyıtextContentile koy.- Boş ya da yalnızca boşluktan oluşan mesaj eklenmesin (eski kod bunları boş satır olarak ekliyordu).
- Gönderince kutu boşalsın.
Beklenen: gg wp listeye eklenir; <b>gg</b> kalın olmaz, harfi harfine görünür; saldırı metni yazı olarak görünür, arka plan kırmızı olmaz; üç boşluk yazıp gönderince hiçbir şey eklenmez.
İpucu 1
Mesajı const metin = mesajKutusu.value.trim() ile al; Ders 9'daki trim baştaki ve sondaki boşlukları atar.
İpucu 2
metin === "" ise return ile erken çık. Değilse createElement("li"), li.textContent = metin, sohbet.append(li).
Çözümü göster
const sohbet = document.querySelector(".sohbet")const mesajKutusu = document.querySelector("#mesaj")const gonder = document.querySelector("#gonder") gonder.addEventListener("click", () => { const metin = mesajKutusu.value.trim() if (metin === "") { return } const li = document.createElement("li") li.textContent = metin sohbet.append(li) mesajKutusu.value = ""})Beş mesajla denedik (gg wp, <b>gg</b>, üç boşluk, boş, saldırı metni): eski kod beş satır ekleyip arka planı kırmızı yaptı; bu kod üç satır ekliyor ve arka plana dokunulmuyor. Elements sekmesinde o li'yi açarsan içinde bir <b> öğesi değil, tırnak içinde "<b>gg</b>" diye bir yazı düğümü görürsün.
Yetenek tuşları
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Yetenekler</title> <link rel="stylesheet" href="gorev4.css"> <script src="gorev4.js" defer></script></head><body> <div class="yetenekler"> <span id="yetenek-q">Q</span> <span id="yetenek-w">W</span> <span id="yetenek-e">E</span> <span id="yetenek-r">R</span> </div> <p class="son">Son yetenek: yok</p></body></html>.yetenekler span { display: inline-block; padding: 8px 14px; border: 2px solid #999; border-radius: 8px;}.aktif { background: gold;}Q, W, E ya da R'ye basınca o yeteneğin kutusu aktif sınıfını alsın ve altta Son yetenek: Q gibi yazsın. Aynı anda yalnızca bir kutu aktif olsun. Başka tuşlar hiçbir şeyi değiştirmesin. Caps Lock açıkken ya da Shift'le basılınca da çalışsın. Örnek: sırayla x, q, e, Shift+r, Enter basınca x'te hiçbir şey değişmez; sonra Q, E, R sırayla aktif olur; Enter'da R aktif kalır.
İpucu 1
Dinleyiciyi document'e tak. Tuşu olay.key.toLowerCase() ile hep küçük harfe çevir (Ders 9); Q ile q aynı olur.
İpucu 2
["q", "w", "e", "r"].includes(tus) ile denetle (Ders 5); değilse return. Shift'in kendisi de bir tuş: "shift" listede olmadığı için kendiliğinden elenir.
İpucu 3
Önce querySelectorAll ve forEach ile bütün kutulardan aktif'i çıkar. Sonra doğru kutuyu document.querySelector(`#yetenek-${tus}`) ile bul ve sınıfı ona ekle.
Çözümü göster
const kutular = document.querySelectorAll(".yetenekler span")const son = document.querySelector(".son")const tuslar = ["q", "w", "e", "r"] document.addEventListener("keydown", (olay) => { const tus = olay.key.toLowerCase() if (!tuslar.includes(tus)) { return } kutular.forEach((kutu) => { kutu.classList.remove("aktif") }) document.querySelector(`#yetenek-${tus}`).classList.add("aktif") son.textContent = `Son yetenek: ${tus.toUpperCase()}`})Denedik: x ve Enter hiçbir şeyi değiştirmiyor; Shift+r R'yi aktif yapıyor; her an tek kutu aktif. includes denetimini atlarsan x'e basınca #yetenek-x aranır, null gelir ve Cannot read properties of null (reading 'classList') hatasını alırsın.
Rol filtresi
Ders 21'deki şampiyon kartları sayfasının menüsü şimdilik yalnızca görüntü: bağlantılara basınca kartlar değişmiyor. Şimdi onu çalıştıracaksın: Büyücü'ye basınca yalnız büyücü kartları kalsın. VS Code'da File → Open Folder ile Ders 21'deki C:\kod\sampiyon-kartlari klasörünü aç; sayfayı yine Go Live ile açacaksın. index.html'de <head>'e betik satırını ekle ve menüdeki dört bağlantıya hangi rolü gösterdiklerini bir data-rol niteliğiyle yaz:
<link rel="stylesheet" href="stil.css"> <script src="filtre.js" defer></script></head><body> <header class="ust"> <h1 class="logo">Şampiyon Kartları</h1> <nav class="menu"> <a href="#" data-rol="hepsi">Hepsi</a> <a href="#" data-rol="buyucu">Büyücü</a> <a href="#" data-rol="savasci">Savaşçı</a> <a href="#" data-rol="nisanci">Nişancı</a> </nav>stil.css'in en sonuna gizleme sınıfını ekle ve aynı klasörde filtre.js dosyasını oluştur:
.gizli { display: none;}Menüdeki bir bağlantıya basınca yalnız o roldeki kartlar görünsün; Hepsi altısını geri getirsin. data-rol değerleri kartların rol sınıflarıyla (buyucu, savasci, nisanci) aynı; kartı sınıfından tanıyacaksın. Bütün menü için tek dinleyici yeter. Bağlantılar href="#" olduğu için tıklayınca adresin sonuna # ekleniyor; bunu da durdur.
Beklenen: Büyücü Ahri ile Lux'ı, Savaşçı Garen ile Yasuo'yu, Nişancı Jinx ile Teemo'yu bırakır; Hepsi altısını gösterir. Aynı bağlantıya ikinci kez basmak sonucu değiştirmez. Bağlantıların arasındaki boşluğa tıklamak hiçbir şeyi değiştirmez ve Console'da hata çıkmaz. Adres çubuğu http://127.0.0.1:5500/index.html olarak kalır.

İpucu 1
Dinleyiciyi .menu'ye tak (olay yetkilendirme). Tıklanan bağlantıyı olay.target.closest("a") ile bul; null ise return.
İpucu 2
Bir bağlantıya tıklandıysa önce olay.preventDefault(), sonra rolü oku: baglanti.dataset.rol. Kartları dinleyicinin dışında, document.querySelectorAll(".kart") ile bir kez bul.
İpucu 3
Her kart için tek soru: gizlensin mi? Rol "hepsi" değilse ve kartta o rolün sınıfı yoksa evet: rol !== "hepsi" && !kart.classList.contains(rol). Bu koşulu classList.toggle("gizli", ...)'a ikinci değer olarak ver.
Çözümü göster
const menu = document.querySelector(".menu")const kartlar = document.querySelectorAll(".kart") menu.addEventListener("click", (olay) => { const baglanti = olay.target.closest("a") if (!baglanti) { return } olay.preventDefault() const rol = baglanti.dataset.rol kartlar.forEach((kart) => { const gizle = rol !== "hepsi" && !kart.classList.contains(rol) kart.classList.toggle("gizli", gizle) })})Denedik: dört bağlantı beklenen kartları bırakıyor, Büyücü'ye iki kez basınca yine Ahri ile Lux kalıyor. Koşulu bir if'e koyup içine ikinci değeri olmayan toggle("gizli") yazsaydın ilk basış doğru çalışırdı ama sonrakiler bozulurdu: Büyücü'den sonra Savaşçı'ya basınca Jinx ile Teemo kalır, Büyücü'ye iki kez basınca altı kart da geri gelirdi. Koşullu toggle her basışta kartın durumunu baştan hesapladığı için bu tuzağa düşmez. Menünün boşluğuna tıklayınca closest null veriyor ve dinleyici sessizce çıkıyor. preventDefault() satırını silince adres …/index.html# oluyor.
Bonus: seçili bağlantıyı vurgula. Bütün bağlantılardan secili sınıfını çıkar, tıklanana ekle; stil.css'te .menu a.secili için bir zemin rengi yaz.
Üç birim, bir yıldız
TFT'de aynı birimden üç tane toplayınca birleşip iki yıldızlı tek birim olurlar. Dükkân uygulamana bunu ekle:
- Bir birim satın alındıktan sonra yedek kulübesinde aynı addan üç tek yıldızlı kart varsa ikisi silinsin, üçüncüsü
Ahri ★★gibi iki yıldızlı olsun vedata-yildiz="2"alsın. Mesaj:Ahri 2 yıldız oldu! - İki yıldızlı birim satılınca maliyetinin üç katı geri gelsin.
Beklenen: iki Ahri iki ayrı kart olarak durur. Üçüncü Ahri'yi alınca tek bir Ahri ★★ kalır. Dördüncü Ahri'de kulübede Ahri ★★ ve yanında tek yıldızlı Ahri olur. 4 altınlık iki yıldızlı Ahri'yi satınca 12 altın gelir.

İpucu 1
Satın alma dinleyicisinde yedek.append(kart) satırından sonra yeni bir fonksiyon çağır: yildizAtlat(kart.dataset.ad). Birleştirme işini o yapsın. Denemek için Console'dan hile yap: altin = 30 yaz, sonra dükkânı dört Ahri ile doldur: dukkan.replaceChildren(kartYap(birimler[6]), kartYap(birimler[6]), kartYap(birimler[6]), kartYap(birimler[6])) ve ekraniGuncelle(). (birimler[6] listedeki Ahri.)
İpucu 2
yedek.querySelectorAll(".kart") ile kulübedeki kartları gez; adı aynı olan ve dataset.yildiz değeri "2" olmayanları bir diziye push et. Dizide üçten az kart varsa return.
İpucu 3
İlk ikisini remove() ile sök. Üçüncüyü yeniden yaratmana gerek yok: dataset.yildiz = "2" ver, içindeki <b>'yi kart.querySelector("b") ile bulup yazısını değiştir. Satış dinleyicisinde kartın dataset.yildiz değeri "2" ise geri ödemeyi üçle çarp.
Çözümü göster
Yeni fonksiyonu yenile fonksiyonunun üstüne ekle:
function yildizAtlat(ad) { const kopyalar = [] yedek.querySelectorAll(".kart").forEach((kart) => { if (kart.dataset.ad === ad && kart.dataset.yildiz !== "2") { kopyalar.push(kart) } }) if (kopyalar.length < 3) { return } kopyalar[0].remove() kopyalar[1].remove() kopyalar[2].dataset.yildiz = "2" kopyalar[2].querySelector("b").textContent = `${ad} ★★` mesaj.textContent = `${ad} 2 yıldız oldu!`}Satın alma dinleyicisinin sonu ve satış dinleyicisinin değişen satırları:
altin = altin - maliyet yedek.append(kart) mesaj.textContent = `${kart.dataset.ad} alındı.` yildizAtlat(kart.dataset.ad) ekraniGuncelle()}) yedek.addEventListener("click", (olay) => { const kart = olay.target.closest(".kart") if (!kart) { return } let deger = Number(kart.dataset.maliyet) if (kart.dataset.yildiz === "2") { deger = deger * 3 } altin = altin + deger kart.remove() mesaj.textContent = `${kart.dataset.ad} satıldı.` ekraniGuncelle()})30 altınla dört Ahri aldık: altın 26, 22, 18, 14'e indi; üçüncüde kulübede tek Ahri ★★ kaldı, dördüncüde yanına tek yıldızlı Ahri geldi. İki yıldızlıyı satınca altın 14'ten 26'ya çıktı (4 × 3 = 12).
Bonus: üç tane iki yıldızlıyı üç yıldızlıya çevirmeyi ya da kulübe doluyken üçüncü kopyayı almaya izin vermeyi (birleşince yer açılır) dene.
Kendini sına
Soru 1
Bu betik Console'a ne yazar? Sayfa bu dersteki index.html, <script> satırında defer var.
const yok = document.querySelector("#mana")console.log(yok)console.log(yok.textContent)Cevabı göster
Önce null yazar: sayfada id="mana" olan bir öğe yok. Sonra üçüncü satırda Uncaught TypeError: Cannot read properties of null (reading 'textContent') hatası gelir. querySelector bulamayınca hata vermez; hata, null'ı kullandığın satırda çıkar.
Soru 2
haber metni "<em>Lux</em> ejderhayı aldı". kutu.textContent = haber ile kutu.innerHTML = haber arasında ekranda ne fark görürsün? Bir oyuncunun yazdığı metin için hangisi güvenli?
Cevabı göster
textContent ile etiketler harfi harfine görünür: <em>Lux</em> ejderhayı aldı. innerHTML ile <em> gerçek bir öğe olur ve Lux eğik yazılır. Oyuncunun yazdığı metin için güvenli olan textContent, çünkü innerHTML metindeki HTML'i kurar ve onerror gibi niteliklerle saldırganın kodunu da çalıştırır (XSS).
Soru 3
Sayfa açılınca ne görürsün? Düğmeye basınca ne olur?
const altinYazisi = document.querySelector(".altin")const dugme = document.querySelector("#topla")let altin = 0 function altinEkle() { altin = altin + 5 altinYazisi.textContent = `Altın: ${altin}`} dugme.addEventListener("click", altinEkle())Cevabı göster
Sayfa Altın: 5 ile açılır, çünkü altinEkle() parantezli olduğu için sayfa yüklenirken hemen bir kez çağrılır. Dönen değer undefined; dinleyiciye fonksiyon değil undefined verilmiş olur. Düğmeye basınca hiçbir şey olmaz, hata da çıkmaz. Doğrusu parantezsiz: addEventListener("click", altinEkle).
Soru 4
Altın sayacında on kez tıklayıp Altın: 10 yaptın. F5'e basınca sayı neden 0'a döner?
Cevabı göster
Tıklamalar yalnızca DOM'u (sayfanın canlı hâlini) ve altin değişkenini değiştirdi; index.html dosyası hiç değişmedi. Yenileyince tarayıcı dosyayı yeniden okur, DOM'u kurar ve betiği baştan çalıştırır: let altin = 0. Değeri yenilemeden sonra da hatırlamak için onu tarayıcıda saklamak gerekir; bunu Ders 23'te localStorage ile yapacaksın.
Soru 5
document.querySelectorAll(".kart").map(...) neden TypeError verir? Nasıl düzeltirsin?
Cevabı göster
querySelectorAll dizi değil NodeList döndürür. NodeList'te length, forEach ve for...of var ama map, filter, find gibi dizi metotları yok. Önce yaymayla diziye çevir: [...document.querySelectorAll(".kart")].map(...).
Soru 6
Sayfa bu dersteki index.html. Önce takım listesinde Jinx'e, sonra Altın: 0 yazısına birer kez tıklıyorsun. Console'a sırayla ne yazılır?
const takim = document.querySelector(".takim") takim.addEventListener("click", (olay) => { console.log(olay.target.textContent)})document.body.addEventListener("click", () => { console.log("body")})Cevabı göster
Üç satır: Jinx, body, body. Jinx'e tıklayınca olay önce li'ye ulaşır, sonra kabarıp ul'ye çıkar: dinleyici çalışır ve olay.target tıklanan li olduğu için Jinx yazar. Olay body'ye de ulaşır, ikinci dinleyici body yazar. Altın: 0 paragrafı ul'nin içinde değil; ondan kabaran olay ul'ye hiç uğramaz, yalnız body duyar.
Soru 7
Dükkân kartlarının her birine ayrı dinleyici takmak yerine neden #dukkan'a tek dinleyici taktık? İki gerekçe söyle.
Cevabı göster
Birincisi, kartlar her yenilemede silinip yeniden yaratılıyor; kartlara tek tek takılan dinleyiciler yeni kartlarda olmazdı (Lux örneğindeki gibi). Olaylar kabarcıklandığı için kapsayıcıdaki tek dinleyici sonradan gelen kartları da yakalar. İkincisi, tek dinleyiciyi yönetmek kolay: bir yerde yazılır, bir yerde düzeltilir. Hangi karta tıklandığını olay.target.closest(".kart") söyler.
Soru 8
kart öğesinin HTML'i <button data-maliyet="3">Jinx</button>. Bu satır ne yazar?
console.log(kart.dataset.maliyet + 2)Cevabı göster
32. dataset'teki değerler her zaman metindir; "3" + 2 metin birleştirme yapar (Ders 1). Hesap için Number(kart.dataset.maliyet) + 2 yaz, 5 çıkar.
Kayıt noktası
Bu dersin dosyalarının çoğu C:\kod\dom klasöründe: alıştırma sayfası, ilk dört görev ve dukkan klasörü. Yeni bir klasör olduğu için Git'i burada bir kez başlatman gerekiyor. VS Code'da Terminal → New Terminal ile terminali aç; C:\kod\dom klasöründeyken:
Bir sonraki derste formlarla çalışacaksın: oyuncunun yazdığı bilgileri alıp doğrulayacak, listeye ekleyip silecek ve sayfa yenilense de kaybolmasınlar diye tarayıcıda localStorage ile saklayacaksın.