Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Modern JavaScript (ES6+)
JavaScript has evolved rapidly since ES6 (2015). Each year brings new features that make the language more expressive and developer-friendly. This chapter covers the essential modern features you should know. ES6 was the biggest single upgrade in JavaScript’s history — it transformed the language from something many developers tolerated into something they genuinely enjoy writing. The features in this chapter are not optional nice-to-haves; they are the vocabulary of modern JavaScript. You will encounter destructuring, arrow functions, modules, and template literals in every codebase, tutorial, and job interview. Understanding them is table stakes.1. Destructuring
Extract values from arrays and objects into distinct variables. Think of destructuring like unpacking a suitcase: instead of pulling items out one by one (const name = person.name; const age = person.age;), you declare a pattern that matches the shape of the data and JavaScript fills in the variables for you.
Array Destructuring
Object Destructuring
Function Parameters
Destructuring Edge Cases
2. Spread Operator
Expand iterables (arrays, strings) or objects.Arrays
Objects (ES2018)
3. Template Literals
Enhanced string formatting with backticks. Before template literals, building strings with variables required awkward concatenation:'Hello, ' + name + '! You are ' + age + ' years old.' Template literals make this natural and readable.
4. Modules (ES6)
Organize code into reusable, isolated files. Before ES6 modules, JavaScript had no built-in module system — the community invented CommonJS (require/module.exports used in Node.js) and AMD. ES6 modules (import/export) are now the standard, supported natively in all modern browsers and Node.js.
Named Exports
Default Exports
ES Modules vs CommonJS — Complete Comparison
| Feature | ES Modules (import/export) | CommonJS (require/module.exports) |
|---|---|---|
| Syntax | import { fn } from './mod.js' | const { fn } = require('./mod') |
| Loading | Static (analyzed at parse time) | Dynamic (executed at runtime) |
| When evaluated | Imports hoisted, executed in dependency order | Executed where require() appears |
| Tree-shaking | Yes (bundlers can eliminate unused exports) | No (entire module is loaded) |
| Conditional imports | No (use dynamic import() instead) | Yes (if (x) require('./mod')) |
Top-level await | Yes (ES2022) | No |
this at top level | undefined | module.exports |
| File extension | .mjs or "type": "module" in package.json | .cjs or default in Node.js |
| Browser support | Native (with type="module") | Not supported (bundler required) |
Dynamic Imports
Load modules on demand (code splitting). Unlike staticimport at the top of a file, dynamic import() returns a Promise and can be called anywhere — inside functions, inside conditionals, in response to user actions. This is how modern bundlers (Webpack, Vite) achieve code splitting.
5. Classes (ES6+)
Syntactic sugar over prototypes with additional features.6. New Data Structures
Map (Key-Value, Any Type Keys)
Unlike plain objects (which only support string/Symbol keys), Map supports keys of any type — objects, functions, numbers, evenNaN.
Map vs Plain Object — Complete Comparison
| Feature | Map | Plain Object {} |
|---|---|---|
| Key types | Any type (objects, functions, numbers) | Strings and Symbols only |
| Key order | Insertion order (guaranteed) | Insertion order (mostly, with numeric key exceptions) |
| Size | map.size (O(1)) | Object.keys(obj).length (O(n)) |
| Iteration | Directly iterable (for...of, .forEach) | Need Object.keys/values/entries first |
| Performance (frequent add/delete) | Optimized for this | Not optimized (hidden class transitions) |
| Prototype pollution risk | None (no inherited keys) | Yes (toString, constructor, etc.) |
| JSON serialization | No (must convert manually) | Yes (native JSON.stringify) |
| Destructuring | No | Yes (const { a, b } = obj) |
| Default choice for | Lookup tables with non-string keys, caches | Config, API payloads, static data |
Set (Unique Values)
A collection that automatically deduplicates. Every value can only appear once.Set vs Array — When to Use Which
| Operation | Set | Array |
|---|---|---|
| Check if value exists | .has() — O(1) | .includes() — O(n) |
| Add value | .add() — O(1) | .push() — O(1) |
| Delete by value | .delete(val) — O(1) | .splice(index, 1) — O(n), requires finding index first |
| Duplicates | Automatically prevented | Allowed (must dedupe manually) |
| Index access | Not supported | arr[i] — O(1) |
| Ordering | Insertion order | Index-based order |
.map(), .filter() | Not directly (convert to array first) | Native |
| JSON serializable | No (must convert to array) | Yes |
| Best for | Unique collections, membership testing, deduplication | Ordered lists, indexed access, transformation chains |
WeakMap and WeakSet
Keys are weakly held — if there are no other references to the key object, it can be garbage collected and the entry is automatically removed. This prevents memory leaks when associating metadata with objects.Map vs WeakMap — When to Use Which
| Feature | Map | WeakMap |
|---|---|---|
| Key types | Any value | Objects only (no primitives) |
| Key retention | Strong (prevents garbage collection) | Weak (allows garbage collection) |
| Iterable | Yes (.forEach, for...of, .keys()) | No |
.size property | Yes | No |
| Use case | General-purpose key-value store | Associating metadata with objects without causing memory leaks |
7. New Array & Object Methods
Array Methods (ES2019+)
Mutating vs Non-Mutating Array Methods
This is one of the most common bug sources in JavaScript. Know which methods change the original array.| Mutates original | Non-mutating equivalent (ES2023+) |
|---|---|
.sort() | .toSorted() |
.reverse() | .toReversed() |
.splice(i, n, ...items) | .toSpliced(i, n, ...items) |
.push() / .pop() | [...arr, item] / arr.slice(0, -1) |
.shift() / .unshift() | arr.slice(1) / [item, ...arr] |
.fill() | No built-in (spread + fill on copy) |
.copyWithin() | No built-in |
Object Methods (ES2017+)
8. Other Modern Features
Optional Chaining & Nullish Coalescing
Logical Assignment (ES2021)
These combine logical operators with assignment — a concise way to set defaults or update values conditionally.Numeric Separators (ES2021)
String Methods (ES2017+)
Summary
Modern JavaScript is expressive, concise, and powerful:- Destructuring: Extract values from objects/arrays elegantly.
- Spread/Rest: Combine, copy, and collect elements.
- Modules: Organize code with
import/export. - Classes: Clean OOP syntax with private fields.
- Map/Set: Powerful data structures beyond objects/arrays.
- Optional Chaining: Safe property access with
?..
Interview Deep-Dive
Explain the difference between shallow copy and deep copy in JavaScript. What are all the ways to achieve each, and what are their trade-offs?
Explain the difference between shallow copy and deep copy in JavaScript. What are all the ways to achieve each, and what are their trade-offs?
Strong Answer:
- A shallow copy duplicates the top-level properties of an object, but nested objects and arrays are shared by reference. A deep copy recursively duplicates everything, so no references are shared between the original and the copy.
- Shallow copy methods: (1) Spread operator:
{...obj}or[...arr]. Clean syntax, widely used. (2)Object.assign({}, obj). Equivalent to spread for objects. (3)Array.from(arr)orarr.slice()for arrays. - Deep copy methods: (1)
structuredClone(obj)(the modern answer, available in browsers and Node 17+). Handles circular references,Date,RegExp,Map,Set,ArrayBuffer, and more. Does NOT copy functions, DOM nodes, or prototype chains. (2)JSON.parse(JSON.stringify(obj))(the legacy hack). Losesundefinedvalues (they are stripped), convertsDateobjects to strings, chokes on circular references (throws), ignoresMap,Set,Symbolkeys, and functions. (3) Lodash_.cloneDeep. Handles almost everything, including functions and custom classes. The trade-off is a dependency. - The bug this question really tests:
const copy = {...original}; copy.address.city = 'NYC';— this also mutatesoriginal.address.citybecauseaddressis a nested object that was not cloned. This is the number one source of state mutation bugs in Redux stores and React state updates. The fix is eitherstructuredCloneor manual nested spreading:{...original, address: {...original.address, city: 'NYC'}}. - Production recommendation: use
structuredClonefor general-purpose deep cloning. Use spread for shallow copies when you know the object is flat. Use Immer (a library) in Redux/React contexts for ergonomic immutable updates without manual deep spreading.
structuredClone uses the structured clone algorithm (the same one used by postMessage and IndexedDB). It cannot clone: functions (throws DataCloneError), DOM nodes (throws), symbols (throws), property descriptors (getters/setters are invoked and their return values are cloned as plain values), the prototype chain (the clone is always a plain object, not an instance of a custom class), and WeakMap/WeakSet (throws). If your object contains any of these, you need a custom clone function or a library. The prototype chain limitation is particularly sneaky: if you structuredClone(new MyClass(...)), the result is a plain Object, not an instance of MyClass. instanceof MyClass returns false on the clone.What are ES modules vs CommonJS modules? Why does the distinction matter, and what problems arise when mixing them?
What are ES modules vs CommonJS modules? Why does the distinction matter, and what problems arise when mixing them?
Strong Answer:
- CommonJS (
require/module.exports): the original Node.js module system. Modules are loaded synchronously.require()can be called anywhere (inside conditionals, inside functions). Exports are live bindings to a cached object — once a module is loaded, subsequentrequire()calls return the cached export. - ES Modules (
import/export): the language-standard module system (ES6). Imports are static — they must be at the top level, not inside conditionals. This enables static analysis: bundlers (Webpack, Vite) can determine the dependency graph at build time and perform tree-shaking (removing unused exports). Exports are live bindings to the original variable (not copies), and they are read-only from the consumer side. - The distinction matters for three reasons: (1) Tree-shaking: only ES modules support it because imports are statically analyzable. If you
import { debounce } from 'lodash-es', the bundler can drop the 90% of lodash you did not use.const _ = require('lodash')pulls in the entire library. (2) Top-level await: only available in ES modules. (3) Dual-package hazard: a Node.js package can be loaded both as ESM and CJS, creating two separate instances of the module. If module A imports the ESM version and module B requires the CJS version, they get different singleton instances, breaking shared state. - Mixing problems:
require()cannot load an ES module directly (it is async).importcan load CJS modules (Node.js wraps them). But the semantics differ: CJS hasmodule.exportsas a single value, while ESM has named exports. When youimporta CJS module, the entiremodule.exportsbecomes the default export, and named imports may not work as expected. This causes “module has no named export” errors that confuse teams. - In Node.js, the module type is determined by: (1) file extension (
.mjs= ESM,.cjs= CJS), or (2)"type": "module"inpackage.json(makes.jsfiles ESM by default).
import { x } from 'module' is a static declaration — the bundler can determine at build time that only x is used, and safely remove all other exports from the bundle. CommonJS require is dynamic: require(condition ? 'a' : 'b') or const lib = require('lib'); lib[dynamicKey]() — the bundler cannot know at build time which parts of the module are used, so it must include everything. The practical impact is significant: a React app importing { useState, useEffect } from React only includes those hooks (plus their dependencies) with tree-shaking. Without it, the entire React library is bundled. For large dependency trees (like date-fns, Material UI, lodash), tree-shaking can reduce bundle size by 60-80%.Explain WeakMap and WeakRef. What problem do they solve that regular Map and closures cannot?
Explain WeakMap and WeakRef. What problem do they solve that regular Map and closures cannot?
Strong Answer:
- The core problem: memory leaks from strong references. If you store an object as a key in a
Mapor in a closure, that reference prevents the garbage collector from reclaiming the object’s memory, even if nothing else in the application needs it. This is fine for data you intend to keep, but it creates leaks when you are associating metadata with objects that have their own lifecycle (DOM nodes, class instances, cache entries). - WeakMap: keys must be objects (not strings or numbers). The key reference is “weak” — it does not prevent garbage collection. If the key object has no other references, it is garbage collected, and the WeakMap entry is automatically removed. You cannot iterate a WeakMap (no
.forEach, no.keys(), no.size) because entries can disappear at any time. - Use cases: (1) Private data for class instances:
const privates = new WeakMap(); class Foo { constructor() { privates.set(this, { secret: 42 }); } getSecret() { return privates.get(this).secret; } }. When aFooinstance is garbage collected, its private data is automatically cleaned up. (2) DOM metadata: associating computed data with DOM nodes without preventing them from being GC’d when removed from the document. (3) Memoization caches where the cache key is an object:const cache = new WeakMap(); function expensiveCompute(obj) { if (cache.has(obj)) return cache.get(obj); const result = /* heavy work */; cache.set(obj, result); return result; }. The cache does not preventobjfrom being collected. - WeakRef (ES2021): provides a weak reference to an object that you can dereference with
.deref(). Returnsundefinedif the object has been garbage collected. Used in conjunction withFinalizationRegistry(which lets you run cleanup code when an object is GC’d). Use case: caches where you want to keep a reference as long as the object exists but do not want to prevent its collection. - Both are advanced features. The typical application developer rarely needs them directly — they are mostly used by library authors, framework internals, and performance-critical systems.
FinalizationRegistry lets you register a callback that fires when a tracked object is garbage collected. const registry = new FinalizationRegistry((heldValue) => { console.log(heldValue + " was collected"); }); registry.register(myObject, "myObject");. The callback receives the “held value” (a plain value, not the object itself — since the object is already gone). You pair it with WeakRef when building a cache: store WeakRef wrappers in a Map, and use a FinalizationRegistry to clean up the map entry when the referenced object is collected. Without the registry, stale WeakRef entries (whose .deref() returns undefined) would accumulate in the map forever. Important caveats: GC timing is non-deterministic, so the callback may fire immediately or much later. You should never rely on FinalizationRegistry for correctness — only for resource cleanup optimization. The spec explicitly warns against using it for anything essential.What are tagged template literals and where are they used in real-world libraries? Explain how they work at a mechanical level.
What are tagged template literals and where are they used in real-world libraries? Explain how they work at a mechanical level.
Strong Answer:Usage:
- A tagged template literal is a function call where the function receives the template literal’s parts as arguments. The syntax is
tagFunction\Hello `. The function receives: (1) an array of string segments (the static parts between interpolations):[‘Hello ’, ’, age ’, ”], and (2) the interpolated values as additional arguments:name, age`. - At a mechanical level: the engine splits the template at each
${}boundary. The string segments array always has one more element than the values array (there is always a string before the first interpolation and after the last). The tag function can process, transform, escape, or completely ignore any of these parts. - Real-world usage: (1) styled-components (CSS-in-JS):
styled.div\color: {props => props.color}; padding: 20px;\`` -- the tag function extracts CSS rules and dynamic values, generates unique class names, and injects CSS into the document head. (2) **GraphQL gql tag**: `gql\`query { user(id: ) }`-- parses the GraphQL string into an AST at build time. (3) **html/sql template tags**: libraries likelit-htmluse tagged templates for efficient DOM rendering. SQL libraries use them for safe parameterized queries:sql`SELECT * FROM users WHERE id = $`-- the tag function ensuresuserId` is properly escaped, preventing SQL injection. - The security angle is key: tagged templates naturally separate trusted static strings from untrusted dynamic values. The tag function knows exactly which parts are developer-authored strings and which are runtime values, making it trivial to escape or sanitize only the dynamic parts. This is fundamentally safer than string concatenation.
- Performance detail: the string segments array is frozen and cached by the engine. If the same tagged template is called multiple times (like in a render loop), the static parts array is the same object every time (referential equality). Tag functions can use this for caching: “If I have seen these exact static parts before, I can skip parsing and reuse the cached result.”
element.innerHTML = safeHTML\$
`. The static HTML structure (, ) passes through untouched, but userInputis HTML-escaped. If the user entered`, it renders as visible text, not executable code. This is exactly the principle that lit-html and other template-based rendering libraries use for XSS protection.