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
| Method | Selector type | Live collection? |
|---|---|---|
querySelector / querySelectorAll | CSS selector | No — static NodeList |
getElementById | id | — |
getElementsByTagName / getElementsByClassName | tag/class | Yes — 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
- Why does
getElementsByTagNamesometimes surprise developers with results changing after the initial call? — It returns a liveHTMLCollection— it automatically reflects DOM changes made after the call, unlikequerySelectorAll's staticNodeListsnapshot; iterating and mutating the DOM at the same time over a live collection is a classic source of skipped or double-processed elements. - 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.targetto determine which specific child was actually clicked. - Why is
elem.textContent = userInputthe correct default overelem.innerHTML = userInputwhen displaying user-supplied text? —innerHTMLparses its argument as HTML markup, so user input containing<script>or event-handler attributes could execute arbitrary code in the page (XSS) —textContentalways 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]]