On this page · sections
JavaScript DOM Events, Scripts & Modules
Can you explain how a click reaches a handler, when a script runs, and how ESM bindings stay live?
Frontend JavaScript sits on three host systems: the DOM event path, the script loader, and the module graph. Interviews mix them because a bug can be any of the three.
These questions test:
- Capture, target, bubble
- Delegation
asyncvsdefervstype="module"- ESM vs CommonJS
- Live bindings and cycles
Questions
DOM
Loading
Modules
Senior-Level Follow-ups
DOM
1. Capture, Target, Bubble
Why This Matters
The path is not “the element then parents.” Capture runs root → target first.
Problem
In which order do listeners fire for a click on a child?
Example
window
capture ↓
document
capture ↓
… ancestors …
capture ↓
target ← target phase
bubble ↑
… ancestors …
bubble ↑parent.addEventListener("click", fn, true); // capture
parent.addEventListener("click", fn); // bubble (default)target listeners run between capture and bubble, in registration order for that element.
Interview Answer
“Events travel capture down, then the target, then bubble up. The third argument or
{ capture: true }selects the phase. Most app handlers use bubble. Capture is for intercepting before children.”
2. Delegation and stopPropagation
Why This Matters
A list of 1,000 rows should not get 1,000 listeners.
Problem
How does delegation work, and what does stopPropagation change?
Example
list.addEventListener("click", (event) => {
const row = event.target.closest("[data-id]");
if (!row || !list.contains(row)) return;
select(row.dataset.id);
});The listener sits on a stable ancestor. New rows work automatically.
event.target is the deepest node. event.currentTarget is the node whose listener is running.
stopPropagation ends the remaining path. stopImmediatePropagation also skips other listeners on the same node.
Interview Answer
“Delegation listens on an ancestor and uses
closestto find the item. It scales with DOM size, not item count. I stop propagation only when I have a real reason — it hides other listeners.”
Loading
3. async vs defer vs Modules
Why This Matters
Script order is a first-paint and a race-condition question.
Problem
When does each script execute, and which preserves order?
Example
| Kind | Fetch | Execute | Order |
|---|---|---|---|
Classic in <head> | blocks parse | immediately | document order |
async | parallel | when ready | not ordered |
defer | parallel | after parse | document order |
type="module" | parallel | after parse | dependency graph |
<script async src="pixel.js"></script>
<script defer src="app.js"></script>
<script type="module" src="entry.js"></script>async is for independent snippets. defer / modules are for app code that needs DOM and a stable order.
Modules are deferred by default, always strict, and evaluate once.
Interview Answer
“Classic scripts block parsing.
asyncruns as soon as the file is ready, unordered.deferand modules wait until the document is parsed and keep order. I put app entry ondeferortype='module'.”
Modules
4. ESM vs CommonJS
Why This Matters
Bundlers hide this until a circular import or a dual-package library breaks in Node.
Problem
What is the difference in loading, exports, and this?
Example
// ESM
import { add } from "./math.js";
export function scale(n) {
return add(n, n);
}// CJS
const { add } = require("./math");
module.exports.scale = (n) => add(n, n);| ESM | CJS | |
|---|---|---|
| Load | Static, async graph | Sync require |
| Exports | Live bindings | Copied values on require |
this at top level | undefined | module.exports |
| File | One evaluation | Same, cached on require.cache |
import() is the dynamic ESM form and returns a Promise.
Interview Answer
“ESM is static and async with live bindings. CJS is synchronous and snapshots
module.exports. I do not mix them casually: default interop (import x from 'cjs') is a bundler/Node policy, not a language identity.”
Senior-Level Follow-ups
5. preventDefault vs Stop Propagation
preventDefault cancels the browser default (navigate, submit, check). It does not stop other listeners.
stopPropagation hides the event from other nodes. You can still preventDefault after a stop if the same listener does both.
{ passive: true } on touch/wheel tells the browser you will not cancel — better scroll performance.
6. Live Bindings and Cycles
// a.js
import { n } from "./b.js";
export const m = 1;
// b.js
import { m } from "./a.js";
export const n = m + 1;ESM can cycle. The imported binding is a live view. Reading it before the exporting module finishes initializing can throw or yield undefined depending on TDZ.
CJS cycles often give a partial exports object. That is why “works in CJS, TDZ in ESM” appears in migrations.
7. Listener Hygiene
Every addEventListener needs a matching remove, or a signal:
const controller = new AbortController();
el.addEventListener("click", onClick, { signal: controller.signal });
controller.abort();Third-party scripts (async pixels) should not be ordered with app bundles. Give them async and a CSP.
Final Interview Strategy
1. Capture then target then bubble
2. Delegate to a stable ancestor
3. async unordered; defer/module ordered after parse
4. ESM live bindings vs CJS snapshot
5. preventDefault ≠ stopPropagation
6. Cycles need initialization order
7. Remove listeners or pass AbortSignalSenior-level signal: Connect the click path, the script schedule, and the module graph. Production bugs usually sit on one of those three timelines.