Modules, Iteration & Generators
Roughly 13% of a typical loop — and 13% of the mock exam here.
ESM versus CommonJS
A static import is resolved and hoisted before any code runs, and may only appear at the top level of a module. That constraint is what lets bundlers build the dependency graph without executing anything — the precondition for tree shaking. require is the opposite: synchronous, evaluated at call time, and legal anywhere.
The subtler difference is live bindings. An ESM import tracks the exporter, so a let count that the exporting module increments is visible as the new value to everyone who imported it (read-only from their side). CommonJS copies the value at assignment time, which is why re-exported CJS values go stale.
Modules also change the ambient rules: strict mode always, a private top-level scope, this is undefined rather than globalThis, and __dirname and require do not exist — import.meta.url replaces them, usually as new URL('./data.json', import.meta.url), a form bundlers statically detect and rewrite.
A module is evaluated once per resolved specifier and cached. Import it from three files and its top-level code runs once, so module-level state is a de facto singleton — convenient for a cache, and a common source of state leaking between tests.
Dynamic import and code splitting
import() returns a promise for the namespace object and is the one form allowed inside a function or condition. Bundlers cut a chunk at each dynamic import, which makes it the standard lazy-loading primitive.
Top-level await is allowed in modules, and it defers not just that module but every importer of it. Cheap for config; expensive on a critical path.
Tree shaking needs static ESM and the absence of side effects the bundler must preserve. A top-level call with observable effects pins the whole module; the sideEffects field in package.json is how a library promises there are none.
Circular imports
In ESM, the second-evaluated module sees bindings that exist but may be uninitialized. Hoisted function declarations survive the cycle; a const or class read too early throws from the temporal dead zone. CommonJS instead hands back a partially filled exports object, so you get undefined rather than an error — quieter and often worse. Either way the real fix is to break the cycle by extracting the shared piece into a third module.
The iterator protocol
An iterable exposes [Symbol.iterator](), returning an object whose next() reports { value, done }. That single protocol powers for...of, spread, destructuring, Array.from, Promise.all and the Map/Set constructors. Spreading a plain object into an array throws “is not iterable” — object spread in { ...obj } is a different feature.
for...of iterates values of an iterable; for...in enumerates enumerable string keys including inherited ones, and over an array it yields index strings. Map and Set guarantee insertion order, unlike plain objects, whose integer-like keys are enumerated first in ascending order.
Breaking out of a for...of loop early calls the iterator’s return() if it has one, so a generator’s finally block still runs — which is what makes generator cleanup reliable.
The easiest way to implement the protocol is a generator method:
class Bag {
#items = [];
*[Symbol.iterator]() { yield* this.#items; }
}
Generators
Calling a generator function runs nothing: it returns a generator object, and the body advances only on next(). That laziness is the point — infinite sequences, pausable state machines, and streaming without materializing arrays.
Communication runs both ways. The argument to gen.next(v) becomes the result of the yield the generator is paused on; gen.throw(err) raises the error at that point, where a try/catch in the body can handle it and resume; gen.return(v) acts like a return there, still running finally. yield* delegates to another iterable and forwards all three.
Async generators combine both worlds: await inside the body, yield per item, and for await...of at the call site — the natural shape for paginated APIs and streams. ES2025 iterator helpers (.map, .filter, .take, .toArray) keep the laziness while reading like array methods, so an infinite source stays usable.
Sample questions
6 of the 26 questions this domain carries in practice mode — expand one to check yourself before drilling.
1. A default export differs from a named export because it:
- may appear once per module and is imported under any name the importer chooses
- is the only export that can be re-exported from an index file without renaming
- is evaluated eagerly, while named exports are resolved the first time they are read
- may appear once per module and must be imported under exactly the name it was declared with
Answer: A. That renaming freedom is why many style guides prefer named exports: a typo in a default import is a new name, not an error.
2. A static import declaration may appear where?
- anywhere in the file, since imports are hoisted to the top before execution
- at the top level, or inside an if statement when the condition is a compile-time constant
- inside any block, as long as the imported binding is not reassigned afterwards
- at the top level of a module only — never inside a function, block or condition
Answer: D. The static form is what lets bundlers build the dependency graph without running the code. For a conditional load, use dynamic import().
3. A module exports let count and later increments it. An importer that read count sees:
- a compile error, since a mutable binding may not be exported from a module
- the original value, because each importer receives a snapshot taken when the module loaded
- the updated value — ESM imports are live bindings, not copies made at import time
- the updated value only after re-importing the module with a dynamic import call
Answer: C. The binding is read-only for the importer but tracks the exporter. CommonJS copies values on assignment to exports, which is the classic difference.
4. Which statement about CommonJS require versus ESM import is accurate?
- both are hoisted, and the only difference is the syntax each module system accepts
- require is synchronous and runs at call time, while static import is resolved and hoisted before execution
- import is synchronous inside Node, and require is the asynchronous form used in bundlers
- require supports live bindings, while import copies each exported value exactly once at the moment the module is loaded
Answer: B. That is why import() exists for conditional loading, and why __dirname and require are absent in ESM — import.meta.url replaces them.
5. gen.throw(err) does what to a generator paused at a yield?
- raises the error at that yield, so a try/catch inside the body can handle it and resume
- rejects the promise the generator returned and marks the iterator as permanently done
- stores the error and delivers it at the next call to next(), one step later than the pause
- terminates the generator immediately, with no chance for the body to catch anything
Answer: A. gen.return(v) is the sibling: it acts like a return at the pause point, still running any finally block before finishing.
6. Importing the same module from three files evaluates its top-level code how many times?
- once per bundle chunk, meaning that a code-split build re-evaluates every shared module once in each generated chunk
- three times, once per importing file, which is why module-level state is never shared
- once per import style, so a static import and a dynamic import produce two instances
- once — modules are cached per resolved specifier, so all three share one namespace and one instance of its state
Answer: D. That single instance is what makes a module-level cache or counter a de facto singleton — and what makes it leak between tests without a reset.
Drill this domain in practice mode →
Independent community study resource — not affiliated with or endorsed by Oracle, Microsoft or Ecma International. JavaScript is a trademark of Oracle Corporation; TypeScript is a trademark of Microsoft Corporation. All questions and study notes are original, written from MDN, the ECMAScript specification and the TypeScript handbook. Everything runs in your browser; nothing you answer is stored or transmitted.