The DOM
Selecting, Creating, inserting, removing, Traversal & attributes, Events, bubbling, capturing, delegation 🎯, Forms & storage
The Document Object Model is a live tree of objects representing the page. JS reads and changes it to make pages interactive. (These APIs are browser-only, not part of ECMAScript.)
Selecting#
document.getElementById("id");
document.querySelector(".card"); // first match (CSS selector) 🟢
document.querySelectorAll("li"); // static NodeList of all matches
element.closest(".panel"); // nearest ancestor matching selector
element.matches(".active"); // boolean
⚠️ querySelectorAll returns a static NodeList; getElementsByClassName/getElementsByTagName return live collections that update as the DOM changes.
Creating, inserting, removing#
const el = document.createElement("div");
el.textContent = "safe text"; // 🟢 no HTML parsing (XSS-safe)
el.classList.add("card");
el.dataset.id = "42"; // data-id="42"
parent.append(el, "text", otherNode); // modern, multiple nodes
parent.prepend(el);
el.before(node); el.after(node); el.replaceWith(node);
el.remove(); // delete self
⚠️ innerHTML parses HTML — never set it from user input (XSS). Prefer textContent. If you must build HTML, sanitize (DOMPurify) or use element.setHTML() where available.
Traversal & attributes#
el.children; el.parentElement; el.nextElementSibling;
el.getAttribute("href"); el.setAttribute("aria-hidden", "true");
el.hidden = true; el.style.color = "red";
el.classList.toggle("open", isOpen);
Events, bubbling, capturing, delegation 🎯#
ELI12. When you click a button inside a list, the click "bubbles" up: button → list → body → document. You can catch it anywhere along the way. Event delegation means putting one listener on the parent instead of many on each child.
graph TD
D[document] -->|capture ↓| B[body]
B --> UL[ul]
UL --> LI[li clicked]
LI -->|bubble ↑| UL
UL --> B2[body]
B2 --> D2[document]
el.addEventListener("click", handler, { capture:false, once:true, passive:true });
el.removeEventListener("click", handler); // must be the SAME function reference
// event delegation — one listener handles many children
list.addEventListener("click", (e) => {
const item = e.target.closest("li");
if (!item) return;
console.log("clicked", item.dataset.id);
});
// prevent default & stop propagation
form.addEventListener("submit", (e) => {
e.preventDefault(); // stop page reload
e.stopPropagation(); // stop bubbling to ancestors
});
- Capturing phase goes top→target; bubbling goes target→top (default
addEventListenerlistens in bubble phase). Pass{capture:true}for the capture phase. passive:truepromises you won't callpreventDefault— lets the browser scroll smoothly.- Delegation benefits: fewer listeners, works for dynamically added elements.
Forms & storage#
const data = new FormData(formEl);
data.get("email");
Object.fromEntries(data); // → plain object
localStorage.setItem("k", JSON.stringify(v)); // persists, ~5MB, strings only, sync
JSON.parse(localStorage.getItem("k"));
sessionStorage; // cleared when tab closes
document.cookie; // small, sent with requests
Observers & misc#
new MutationObserver(cb).observe(node, { childList:true, subtree:true });
new IntersectionObserver(cb).observe(el); // visibility (lazy-load, infinite scroll)
new ResizeObserver(cb).observe(el); // element size changes
history.pushState({}, "", "/new-url"); // SPA routing
navigator.clipboard.writeText("copied"); // async clipboard
Best practices: batch DOM writes (reads then writes) to avoid layout thrashing; use documentFragment or requestAnimationFrame for bulk updates; prefer textContent over innerHTML; remove listeners you no longer need.
Interview Q&A#
Q1. Explain event bubbling vs capturing and event delegation's benefits.
Events travel down from the document to the target (capture phase) then back up (bubble phase); addEventListener listens in the bubble phase by default, and {capture:true} opts into capture. Event delegation puts one listener on a common ancestor and inspects e.target.closest(...), giving fewer listeners and automatic coverage of dynamically added children.
Q2. innerHTML vs textContent — security and performance.
innerHTML parses its string as HTML, so setting it from user input enables XSS; textContent inserts plain text with no parsing and is XSS-safe and faster. Prefer textContent; if you must inject HTML, sanitize it (e.g. DOMPurify) first.
Q3. Live vs static NodeLists.
querySelectorAll returns a static NodeList — a snapshot that doesn't change when the DOM changes. getElementsByClassName/getElementsByTagName return live collections that auto-update, which can cause surprising behavior (e.g. infinite loops) if you mutate the DOM while iterating.
Q4. Why must removeEventListener receive the same function reference?
Listeners are keyed by function identity, so removal only works if you pass the exact same reference used in addEventListener. An inline arrow or a fresh .bind(this) creates a new function each time and can never be removed — store the reference in a variable first.