Back to Notes

JS DOM & Events

Nodes vs. Elements

Nodes include everything (comments, text, elements) — navigated via childNodes, firstChild, nextSibling. Elements are HTML tags only — navigated via children, firstElementChild, nextElementSibling. In interviews, almost always use Element navigation (children) to avoid dealing with accidental whitespace text nodes that childNodes picks up.

Searching the DOM

MethodSelector typeLive collection?
querySelector / querySelectorAllCSS selectorNo — static NodeList
getElementByIdid—
getElementsByTagName / getElementsByClassNametag/classYes — HTMLCollection

"Live" matters concretely: a getElementsByTagName('div') collection automatically grows if a new <div> is added to the DOM after the call — a querySelectorAll result never updates, it's a frozen snapshot at call time. querySelector/querySelectorAll are by far the most common choice in practice; getElementsBy* shows up mostly in older code or when the live-updating behavior is specifically wanted.

Also useful: elem.matches(css) (does this element match a selector), elem.closest(css) (nearest matching ancestor, including itself), elemA.contains(elemB) (descendant check).

Attributes vs. Properties

Attribute: written in HTML, a string, read via getAttribute(). Property: the live DOM object field, read via direct access (elem.className). Standard properties (id, href) sync between the two; non-standard ones don't.

// The classic attribute/property divergence gotcha
input.getAttribute('value')   // the ORIGINAL HTML value, frozen at page load
input.value                    // the CURRENT value, reflecting live user input

Modifying the DOM

document.createElement('div');
node.append(...nodes);      // insert at end
node.prepend(...nodes);     // insert at start
node.remove();
node.cloneNode(true);       // deep clone; false = shallow

Event Propagation — Three Phases

Capture (top → target) → Target → Bubble (target → top)
elem.addEventListener('click', handler);                       // bubble phase (default)
elem.addEventListener('click', handler, { capture: true });    // capture phase

event.stopPropagation();   // stops travel in either direction
event.preventDefault();    // stops the browser's default action — does NOT stop propagation

preventDefault and stopPropagation solve two genuinely different problems — confusing them is a real interview tell. preventDefault stops the browser from doing something (submitting a form, following a link); stopPropagation stops the event from continuing to travel to other listeners.

Event Delegation

Attach one listener to a parent instead of many to individual children — relies on bubbling.

document.getElementById('list').addEventListener('click', (e) => {
  if (e.target.matches('li')) console.log('Clicked:', e.target.textContent);
});

Why this matters beyond "fewer listeners": it automatically works for children added to the DOM after the listener was attached (a dynamically-rendered list item added later still triggers the parent's listener when clicked) — attaching individual listeners per child would require re-attaching on every DOM update.

mouseenter vs. mouseover — a Real Gotcha

mouseenter fires once when the cursor enters the element itself, does not bubble. mouseover fires when the cursor enters the element or any descendant, and does bubble. Using mouseover for a hover effect on a parent with child elements causes repeated firing as the cursor crosses child boundaries — mouseenter is almost always what's actually intended for a simple hover state.

Performance: Layout Thrashing

// Bad — interleaved read/write forces a layout recalculation on every iteration
boxes.forEach(box => {
  const width = box.offsetWidth;          // READ — forces layout
  box.style.width = (width * 2) + "px";  // WRITE
});

// Good — batch all reads, then all writes
const widths = boxes.map(box => box.offsetWidth);
boxes.forEach((box, i) => { box.style.width = (widths[i] * 2) + "px"; });

Reading a layout-dependent property (offsetWidth, getBoundingClientRect()) forces the browser to synchronously recompute layout if a pending write hasn't been applied yet — alternating read/write in a loop forces this recomputation on every iteration; batching all reads before all writes lets the browser compute layout once.

innerHTML vs textContent — a Security-Relevant Distinction

elem.innerHTML = userInput;     // DANGEROUS — parses tags, real XSS vector with untrusted input
elem.textContent = userInput;   // SAFE — always rendered as literal text, never parsed as markup

Always use textContent for user-supplied content — this is not a style preference, it's the direct defense against a real injection class, the DOM equivalent of [[SQL Security & Injection Defense]]'s parameterized-query defense against SQL injection.

Interview Drills

  1. Why does getElementsByTagName sometimes surprise developers with results changing after the initial call? — It returns a live HTMLCollection — it automatically reflects DOM changes made after the call, unlike querySelectorAll's static NodeList snapshot; iterating and mutating the DOM at the same time over a live collection is a classic source of skipped or double-processed elements.
  2. Why does event delegation work for dynamically added child elements without re-attaching any listeners? — The listener lives on the stable parent, and relies on event bubbling — a click on a child (even one added long after the listener was attached) still bubbles up to the parent, where the single listener checks e.target to determine which specific child was actually clicked.
  3. Why is elem.textContent = userInput the correct default over elem.innerHTML = userInput when displaying user-supplied text? — innerHTML parses its argument as HTML markup, so user input containing <script> or event-handler attributes could execute arbitrary code in the page (XSS) — textContent always renders its argument as literal text, with no markup interpretation, making it immune to this injection class by construction.

Cross-links

[[JS Core Language & Data Types]] · [[SQL Security & Injection Defense]] · [[React Performance]]