Most of what JavaScript gained between 2023 and 2026 is here: Set algebra, iterator helpers, grouping, upsert, Array.fromAsync, base64 on Uint8Array. Interviewers use collections questions to see whether you pick the right structure ("why a Map here?") and whether you know the protocol underneath for...of, which is what makes generators, streams and lazy pipelines click.
size, no prototype to collide with (__proto__), faster for frequent add/remove and unknown key sets; objects stay for fixed-shape records and JSON. Set for uniqueness and membership (has is O(1) versus includes O(n)); both use SameValueZero (NaN equal to itself, +0/-0 equal).union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom, returning new Sets and accepting set-likes.size, entries vanish when the key is collected. The right tool for caches keyed by objects (DOM nodes, component instances) and private data. WeakRef and FinalizationRegistry exist for advanced cases and are not a cleanup mechanism you can rely on.[Symbol.iterator]() returning an iterator with next() → { value, done } (optional return()/throw()). Arrays, strings, Map, Set, arguments, NodeList, typed arrays are iterable; for...of, spread, destructuring, Array.from, Promise.all consume iterables. Async iterables use [Symbol.asyncIterator] and for await...of.function*) build iterators from imperative code: yield pauses, yield* delegates, return finishes, return() from the consumer triggers finally for cleanup; they are lazy and can be infinite. Async generators (async function*) yield promises and are how you wrap paginated APIs or streams.map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, find, plus Iterator.from(); lazy, so take(5) on an infinite generator terminates. Iterator.concat (ES2026) chains iterables; Iterator.zip ("joint iteration") is finished for ES2027.Object.groupBy / Map.groupBy (ES2024) replace reduce-based grouping; Map.prototype.getOrInsert / getOrInsertComputed (ES2026 "upsert") replace the if (!map.has(k)) map.set(k, init) dance.toSorted, toReversed, toSpliced, with (ES2023) versus the mutating sort/reverse/splice; at(-1), findLast, flat/flatMap; Array.fromAsync (ES2026) collects an async iterable. sort is stable and compares as strings without a comparator.ArrayBuffer (resizable and transfer() since ES2024), typed arrays, DataView, Float16Array (ES2025); Uint8Array.fromBase64/toBase64/fromHex/toHex (ES2026) end the btoa workarounds.ReadableStream (async iterable), WritableStream, TransformStream, pipeThrough/pipeTo, backpressure built in; fetch bodies are streams (response.body), TextDecoderStream/TextEncoderStream convert, and Nitro can return a ReadableStream as a response. Streams are how you process a 200 MB response without holding it in memory.structuredClone deep-copies Map, Set, Date, ArrayBuffer and more (not functions or DOM nodes); spread is shallow.Right structure, right method:
// Index by id once, look up O(1) instead of nested find (O(n²)).
const byId = new Map(users.map(u => [u.id, u] as const))
const withOrders = orders.map(o => ({ ...o, user: byId.get(o.userId) }))
// Grouping (ES2024) and upsert (ES2026)
const byRole = Map.groupBy(users, u => u.role) // Map<Role, User[]>
const counts = new Map<string, number>()
for (const u of users) counts.set(u.country, counts.getOrInsert(u.country, 0) + 1)
// Set algebra (ES2025)
const admins = new Set(users.filter(u => u.role === 'admin').map(u => u.id))
const online = new Set(presence.ids)
const onlineAdmins = admins.intersection(online)
const offlineAdmins = admins.difference(online)
// Lazy pipeline with iterator helpers: nothing runs until take() pulls
function* naturals() { let n = 1; while (true) yield n++ }
const firstFiveEvenSquares = naturals().filter(n => n % 2 === 0).map(n => n * n).take(5).toArray()
// Non-mutating sort for a computed, then base64 for a binary payload (ES2026)
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
const b64 = new TextEncoder().encode(JSON.stringify(sorted)).toBase64()
An async generator over a paginated API, consumed lazily, and Array.fromAsync to collect it:
export async function* paginate<T>(url: string, signal?: AbortSignal) {
let cursor: string | null = null
do {
const page: { items: T[], next: string | null } = await $fetch(url, { query: { after: cursor }, signal })
yield* page.items // hand items out one by one; the caller decides when to stop
cursor = page.next
} while (cursor)
}
// Stop after the first match without fetching the rest of the pages:
for await (const item of paginate<Product>('/api/products')) {
if (item.sku === wanted) { found = item; break } // break calls the generator's return(): the loop ends, no more fetches
}
// Or collect everything (careful with size):
const all = await Array.fromAsync(paginate<Product>('/api/products'))
Streaming a large response through a transform without buffering it:
export default defineEventHandler(async (event) => {
const upstream = await fetch('https://warehouse.example.com/rows.ndjson')
const toCsv = new TransformStream<string, string>({
transform(chunk, controller) {
for (const line of chunk.split('\n').filter(Boolean)) {
const row = JSON.parse(line) as { id: string, name: string }
controller.enqueue(`${row.id},"${row.name.replaceAll('"', '""')}"\n`)
}
},
})
setResponseHeader(event, 'content-type', 'text/csv; charset=utf-8')
return upstream.body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(toCsv)
.pipeThrough(new TextEncoderStream())
})
[10, 9, 1].sort() gives [1, 10, 9]: the default comparison is by string. Always pass a comparator, and for user-visible strings use localeCompare or an Intl.Collator so accents and case sort correctly.
computed(() => items.value.sort(...)) mutates the source array and triggers itself. toSorted (ES2023) is the non-mutating version and is exactly what a derived value should use.
Browser and Node availability of the ES2026 additions (Map.prototype.getOrInsert, Iterator.concat, Uint8Array.toBase64) as of the interview week; polyfill or transpile until they are Baseline.
find join over two arrays with a Map index and measure both at 10,000 items.break stops the fetching by logging each page request."A Map when keys are dynamic or not strings, when insertion order or size matters, or when I add and remove often: lookups are O(1) with any key type and there is no prototype to collide with. Objects stay for fixed-shape records and JSON. A Set when the question is membership or uniqueness, because has is constant time where includes scans, and since ES2025 Sets have union, intersection and difference built in. WeakMap when the key is an object whose lifetime I do not control, so the entry disappears with it. And I reach for the iteration protocol underneath all of them, generators and iterator helpers, when I want lazy pipelines that stop early."
"An iterable is anything with a Symbol.iterator method that returns an iterator: an object whose next() returns { value, done }. for...of, spread, destructuring, Array.from and Promise.all all consume that protocol, and Map, Set, strings and arrays implement it. Generators are the easiest way to produce one: yield pauses the function and hands out a value on demand, so sequences can be lazy or infinite, and return() from the consumer runs the generator's cleanup. Async generators do the same over promises, which is how I wrap a paginated API so for await...of can stop after the first match without fetching the remaining pages. Iterator helpers since ES2025 add map, filter and take directly on iterators, so the pipeline stays lazy."
Nothing yet. Notes filed here show up on this page and the section overview.
Modules and bundling
ESM semantics — static structure, live bindings, singleton modules, cycles and top-level await — versus CommonJS, Node interop and package exports, tree shaking and code splitting, and what Vite and Rolldown do with all of it.
Memory and performance in JavaScript
How garbage collection decides what lives, the leak patterns that survive it, finding retainers with heap snapshots, and the performance model — Big-O in practice, hot paths, long tasks, and measuring before optimising.