İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

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 querySelector ve querySelectorAll ile istediğin öğeyi bulabilirsin
  • <script> etiketini defer ile doğru yere koyabilir, null hatasının sebebini açıklayabilirsin
  • Yazıyı textContent ile güvenle değiştirebilir, innerHTML'in XSS tehlikesini bir örnekle gösterebilirsin
  • classList, style, createElement, append ve remove ile sayfanın görünümünü ve yapısını değiştirebilirsin
  • addEventListener ile 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
  1. Sayfanın canlı hâli: DOM
  2. İlk betik: Console artık tarayıcıda
  3. Öğe bulmak: querySelector
  4. defer: betik ne zaman çalışır?
  5. İçeriği değiştirmek: textContent ve innerHTML
  6. Görünümü değiştirmek: classList ve style
  7. Öğe oluşturmak: createElement, append, remove
  8. Olaylar: sayfa sana haber verir
  9. Olay yetkilendirme: tek dinleyici, çok öğe
  10. Uygulama: TFT dükkânı
  11. Sık hatalar
  12. Görevler
  13. Kendini sına
  14. 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:

index.html
<!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:

stil.css
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:

document html head body title link script (ve iki meta) h1 p .altin button #topla ul .takim "Altın Sayacı" "Altın: 0" "Altın topla" li li li "Ahri" "Garen" "Jinx" kök: document öğe düğümü (bir etiket) "…" yazı düğümü
Şekil 22.1 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:

Console
> 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:

sayac.js — dom — Visual Studio Code
sayac.js
console.log("Merhaba, tarayıcı")console.log(document.title)
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Bu sefer çıktı VS Code'un terminaline gelmez. Tarayıcıda F12 ile Console'u aç:

Console (F12)
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:

Sık hataReferenceError: document is not defined

Ne 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.

sayac.js
console.log("Merhaba, tarayıcı")console.log(document.title)
Hata çıktısı
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.0

Nası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:

sayac.js
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)
Console (F12)
Altın SayacıAltın: 0Altın toplaAhri

document.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çiciNeyi bulurBu sayfada
"h1"etiket adına göreilk h1
".altin"class niteliğinde altin olanAltın: 0 paragrafı
"#topla"id'si topla olandüğme
".takim li".takim'ın içindeki liilki: Ahri

Seçiciye uyan öğe yoksa querySelector hata vermez, null verir (Ders 12'de gördüğün "burada bir şey yok" değeri):

sayac.js
const mana = document.querySelector(".mana")console.log(mana)
Console (F12)
null

Bu 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:

sayac.js
const sampiyonlar = document.querySelectorAll(".takim li")console.log(sampiyonlar.length) sampiyonlar.forEach((li) => {    console.log(li.textContent)})
Console (F12)
3AhriGarenJinx

Dö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:

sayac.js
const sampiyonlar = document.querySelectorAll(".takim li")const adlar = sampiyonlar.map((li) => li.textContent)
Console (F12)
Uncaught TypeError: sampiyonlar.map is not a function    at sayac.js:2:27

map 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.

sayac.js
const sampiyonlar = document.querySelectorAll(".takim li")const adlar = [...sampiyonlar].map((li) => li.textContent)console.log(adlar.join(", "))
Console (F12)
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.

Sık hataUncaught 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:

sayac.js
const altinYazisi = document.querySelector("altin")altinYazisi.textContent = "Altın: 300"
Console (F12)
Uncaught TypeError: Cannot set properties of null (setting 'textContent')    at sayac.js:2:25

Nası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:

sayac.js
const baslik = document.querySelector("h1")console.log(baslik.textContent)
index.html✗ Yanlış
    <script src="sayac.js"></script>
Console (F12)
Uncaught TypeError: Cannot read properties of null (reading 'textContent')    at sayac.js:2:20

Seç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.

tarayıcı HTML'i okur head içinde, defer yok head sayac.js ✗ body (h1, p, button…) body daha yok: querySelector → null body'nin en sonunda head body (h1, p, button…) sayac.js ✓ head içinde, defer ile önerilen head body (h1, p, button…) sayac.js ✓ sayac.js bu arada arka planda iner
Şekil 22.2 Aynı sayac.js, üç ayrı yerde. Sorun betiğin kendisinde değil, ne zaman çalıştığında.

İki çözüm var:

  • <script>'i body'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ıp defer eklemek: "betiği indir ama HTML'in tamamı okunana kadar çalıştırma" demek. Tarayıcı bu arada sayfayı okumayı sürdürür, birden çok defer'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:

sayac.js
let altin = 300const altinYazisi = document.querySelector(".altin") console.log(altinYazisi.textContent)altinYazisi.textContent = `Altın: ${altin}`console.log(altinYazisi.textContent)
Console (F12)
Altın: 0Altın: 300

Sayfada 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:

index.html
    <p class="mesaj"></p>
sayac.js
const haber = "<strong>Ahri</strong> çift öldürme yaptı!"const mesajKutusu = document.querySelector(".mesaj") mesajKutusu.textContent = haber

Sonra son satırı mesajKutusu.innerHTML = haber yap ve farka bak:

127.0.0.1:5500/index.html
textContent ile: sayfanın altında harfi harfine strong Ahri strong çift öldürme yaptı yazıyor.
textContent: etiketler harf olarak görünüyor.
127.0.0.1:5500/index.html
innerHTML ile: sayfanın altında Ahri kalın harflerle, ardından çift öldürme yaptı yazı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?

sayac.js
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 = gelenMesaj

innerHTML 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:

127.0.0.1:5500/index.html
innerHTML ile: sayfadaki her şey silinmiş, yalnızca Bu sayfayı artık saldırgan yönetiyor yazısı kalmış.
innerHTML ile: sayfanın tamamı gitti.
127.0.0.1:5500/index.html
textContent ile: sayfa yerinde duruyor, en altta saldırı metni harfi harfine bir yazı olarak görünüyor.
Aynı mesaj 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.

<img src="yok.png" onerror="…"> kullanıcıdan gelen metin textContent ile p "<img src=… onerror=…>" Yalnız bir yazı düğümü. Ekranda harfi harfine görünür, kod çalışmaz. innerHTML ile p img Gerçek bir img öğesi kurulur. Resim yüklenemez, onerror'daki kod çalışır.
Şekil 22.3 Aynı metin iki yoldan geçiyor. 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 hep textContent.
  • Yeni bir öğe gerekiyorsa birazdan göreceğin createElement'i kullan, içine yazıyı yine textContent ile 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:

stil.css
.zengin {    color: darkgoldenrod;    font-weight: bold;}

Bir öğenin sınıflarını classList ile yönetirsin:

sayac.js
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)
Console (F12)
truefalsetruealtin zengin
MetotNe 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:

sayac.js
const altinYazisi = document.querySelector(".altin")let altin = 120 altinYazisi.textContent = `Altın: ${altin}`altinYazisi.classList.toggle("zengin", altin >= 100)console.log(altinYazisi.className)
Console (F12)
altin zengin

style: 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:

index.html
    <div class="can">        <div class="can-dolu"></div>    </div>
stil.css
.can {    width: 240px;    height: 16px;    background: #ddd;    border-radius: 8px;}.can-dolu {    width: 100%;    height: 100%;    background: seagreen;    border-radius: 8px;}
sayac.js
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:

127.0.0.1:5500/index.html
Sayfanın altındaki can barının yüzde yetmişi yeşil.
hp = 70: barın %70'i dolu.
127.0.0.1:5500/index.html
Sayfanın altındaki can barının yalnızca dörtte biri dolu ve kırmızı.
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 = 70 yazarsan tarayıcı satırı sessizce yok sayar: bar değişmez, hata da çıkmaz.
  • style yalnızca öğenin kendi style niteliğine yazar ve oradan okur. Ders 20'den bildiğin gibi style niteliği bütün seçicileri yener; crimson bu yüzden stil.css'teki seagreen'in önüne geçti. Tersi olmaz: seagreen'i canBari.style.backgroundColor ile 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.
Sık hataUncaught SyntaxError: Invalid left-hand side in assignment

Ne zaman çıkar? CSS özelliğini JavaScript'te tireli adıyla yazınca:

sayac.js
const canBari = document.querySelector(".can-dolu")canBari.style.background-color = "crimson"
Console (F12)
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:

sayac.js
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)
Console (F12)
<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.

1 createElement("li") 2 textContent = "Lux" 3 takim.append(yeni) li bellekte duruyor, sayfada yok li "Lux" içi doldu, hâlâ sayfada yok ul Ahri Garen Jinx Lux ağaca takıldı, ekranda görünür
Şekil 22.4 Yaratmak, doldurmak, takmak. Ağaca takılmayan öğe ekranda görünmez.

Bir diziden liste kurmak da aynı kalıp, yalnızca döngünün içinde. Öğe silmek için remove() yeter:

sayac.js
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)
Console (F12)
5

Liste artık Garen, Jinx, Teemo, Annie, Yasuo: üç öğe eklendi, Ahri remove() ile ağaçtan söküldü. Bilmen gereken birkaç şey daha:

  • append hep sona ekler; başa eklemek için prepend.
  • append birden ç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 append edersen 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 appendChild görürsün; yalnızca tek öğe alır. append onun 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:

sayac.js
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)
Console (F12)
<li data-maliyet="3">Jinx</li>314

dataset.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:

sayac.js
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)
127.0.0.1:5500/index.html
Sayfa açılmış: Altın: 0 yazıyor.
Sayfa açıldığında
127.0.0.1:5500/index.html
Üç tıklamadan sonra: Altın: 3 yazıyor.
Düğmeye üç kez tıklayınca

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.

Sık hataFonksiyonu çağırıp vermek: altinEkle()

Ne zaman çıkar? Dinleyiciye fonksiyonun adı yerine onun çağrısını verince:

sayac.js✗ Yanlış
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:

sayac.js
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:

sayac.js
const dugme = document.querySelector("#topla") dugme.addEventListener("click", (olay) => {    console.log(olay.type)    console.log(olay.target.textContent)})
Console (F12) · düğmeye bir kez tıklayınca
clickAltın topla

olay.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:

index.html
    <input id="ad" placeholder="Sihirdar adın">    <p class="selam">Merhaba, sihirdar!</p>
sayac.js
const adKutusu = document.querySelector("#ad")const selam = document.querySelector(".selam") adKutusu.addEventListener("input", () => {    console.log(adKutusu.value)    selam.textContent = `Merhaba, ${adKutusu.value}!`})
Console (F12) · kutuya Ahri yazınca
AAhAhrAhri

input 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.

Sık hataKutudaki yazı boş geliyor: input'u textContent ile okumak

Ne zaman çıkar? Bir input'un içindekini textContent ile okumaya çalışınca:

sayac.js✗ Yanlış
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

sayac.js
document.addEventListener("keydown", (olay) => {    console.log(olay.key)})
Console (F12) · sırayla d, Shift+D, f, Enter, yukarı ok, Esc
dShiftDfEnterArrowUpEscape

Dinleyiciyi 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:

sayac.js
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:

index.html
    <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>
stil.css
.gizli {    display: none;}
sayac.js
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:

stil.css
.secili {    background: gold;}

İlk akla gelen, querySelectorAll ile hepsini bulup her birine dinleyici takmak:

sayac.js
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:

sayac.js
document.querySelector(".takim").addEventListener("click", () => {    console.log("ul duydu")})document.body.addEventListener("click", () => {    console.log("body duydu")})document.addEventListener("click", () => {    console.log("document duydu")})
Console (F12) · Garen'e bir kez tıklayınca
ul duydubody duydudocument duydu

Tek tıklama, üç dinleyici. Garen'e tıkladın ama onun bütün ataları da haber aldı.

body ul.takim li Ahri li Garen tıklandı li Jinx document html body ul.takim dinleyici burada, çalışır li (olay.target)
Şekil 22.5 Tıklama önce 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:

sayac.js
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)
127.0.0.1:5500/index.html
Takım listesinde Garen ve Lux satırlarının arka planı sarı; Ahri ve Jinx seçili değil.
Garen'e ve sonradan eklenen Lux'a tıklandı: ikisi de seçildi.

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:

127.0.0.1:5500/dukkan/index.html
Koyu zeminli dükkân: solda sarı Altın: 10 yazısı, sağda Altın topla ve Yenile düğmeleri. Altında beş kart: Lux 3 altın, Annie 2 altın, Lux 3 altın, Aurelion Sol 5 altın, Jinx 3 altın. En altta boş Yedek kulübesi 0/9.
Dükkân açıldığında: 10 altın ve rastgele beş kart. Kenar rengi maliyeti gösteriyor.

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

dukkan/index.html
<!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.

dukkan/stil.css
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ı:

dukkan/dukkan.js
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 bir button: fareyle olduğu kadar Tab ve Enter ile de seçilebilsin diye (Ders 19'daki erişilebilirlik). classList.add aynı anda iki sınıf ekliyor; dataset kartı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 karta pahali sınıfını koşullu toggle ile takar ya da çıkarır.
  • dukkaniDoldur, eski kartları replaceChildren() ile söker ve Ders 6'daki Math.random ile beş rastgele kart koyar.

Adım 3: olaylar

dukkan.js'in ikinci yarısı; dosyanın sonuna ekle:

dukkan/dukkan.js
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/9 doluyken 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.
127.0.0.1:5500/dukkan/index.html
Dükkânda Altın: 1. Aurelion Sol ve Yasuo kartları soluk; Teemo, Teemo ve Garen normal. Yedek kulübesinde Lux ve Annie var. Altta Altının yetmiyor yazıyor.
Lux ile Annie alındı, iki yenilemeden sonra 1 altın kaldı. 5 ve 2 altınlık kartlar soluk; birine tıklanınca uyarı çıkıyor.

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.

Sık hataUncaught 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).

sayac.js
const dugme = document.querySelector("#topla") dugme.addEventListener("click", () => {    console.log("Altın +1")})
Console (F12) · script head'de, defer yok
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')    at sayac.js:3:7

Nası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.

Sık hataUncaught TypeError: document.querySelectorAll(...).addEventListener is not a function

Ne zaman çıkar? Bir öğe listesine (NodeList) doğrudan dinleyici takmaya çalışınca:

sayac.js
document.querySelectorAll(".takim li").addEventListener("click", () => {    console.log("tıklandı")})
Console (F12)
Uncaught TypeError: document.querySelectorAll(...).addEventListener is not a function    at sayac.js:1:40

Nası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.

Sık hataSınıf adında boşluk: InvalidCharacterError

Ne zaman çıkar? classList.add'e boşluklu tek bir metin verince:

sayac.js
const altinYazisi = document.querySelector(".altin")altinYazisi.classList.add("zengin parlak")
Console (F12)
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:23

Nası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").

Sık hataBetik hiç çalışmıyor: 404 (Not Found) ve Refused to execute script

Ne 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:

Console (F12)
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ç.

Görev 1

Mana iksiri

gorev1.html
<!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>
gorev1.css
.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:

Metin
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
gorev1.js
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.

Görev 2

Maç notu sayacı

gorev2.html
<!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>
gorev2.css
.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
gorev2.js
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örev 3

Güvenli sohbet

Bir arkadaşın sohbet kutusu yazmış ama kodu XSS'e açık:

gorev3.html
<!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>
gorev3.js✗ Yanlış
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:

  • innerHTML kullanma. Her mesaj için createElement ile bir li yarat, yazıyı textContent ile 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
gorev3.js
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.

Görev 4

Yetenek tuşları

gorev4.html
<!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>
gorev4.css
.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
gorev4.js
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.

Görev 5

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:

index.html
    <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:

stil.css
.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.

127.0.0.1:5500/index.html
Ders 21'deki şampiyon kartları sayfası. Üstte Şampiyon Kartları başlığı, sağda Hepsi, Büyücü, Savaşçı, Nişancı bağlantıları. Altında yalnız iki kart var: Ahri ve Lux, ikisi de mor büyücü rengiyle.
Beklenen: Büyücü'ye basınca yalnız Ahri ile Lux kaldı.
İ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
filtre.js
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.

Görev 6

Üç 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 ve data-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.

127.0.0.1:5500/dukkan/index.html
Dükkân boş, altın 14. Yedek kulübesinde iki yıldızlı Ahri ile tek yıldızlı Ahri kartları yan yana; altta Ahri alındı yazıyor.
Beklenen: dört Ahri aldıktan sonra kulübede bir iki yıldızlı, bir tek yıldızlı.
İ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:

dukkan/dukkan.js
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ı:

dukkan/dukkan.js
    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.

JavaScript
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?

JavaScript
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?

JavaScript
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?

JavaScript
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.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.