Skip to main content

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

const colors = ['red', 'green', 'blue'];

// Basic -- position-based (first variable gets first element, etc.)
const [first, second, third] = colors;
// first = 'red', second = 'green', third = 'blue'

// Skip elements with empty commas
const [, , lastColor] = colors; // 'blue'

// Rest operator (...) collects remaining elements into a new array
const [primary, ...others] = colors;
// primary = 'red', others = ['green', 'blue']
// Note: rest element must be LAST -- [...others, last] is a syntax error

// Default values kick in when the destructured value is undefined
const [a, b, c, d = 'yellow'] = colors;
// d = 'yellow' (default used because colors[3] is undefined)

// Swap variables -- no temporary variable needed!
let x = 1, y = 2;
[x, y] = [y, x]; // x = 2, y = 1
// This works because the right side is evaluated fully before assignment

Object Destructuring

const person = { name: 'Alice', age: 25, city: 'NYC' };

// Basic -- variable names must match property names
const { name, age } = person;

// Rename -- use colon syntax: { propertyName: newVariableName }
// This reads as "take 'name' and call it 'personName'"
const { name: personName, age: personAge } = person;

// Default values -- used when property is undefined (not when null!)
const { name, country = 'USA' } = person;

// Nested destructuring -- follow the shape of the object
const user = {
    id: 1,
    profile: { firstName: 'Alice', lastName: 'Smith' }
};
const { profile: { firstName } } = user; // 'Alice'
// CAUTION: 'profile' is NOT defined as a variable here -- only 'firstName' is.
// If you also need profile: const { profile, profile: { firstName } } = user;

// Rest -- collects all remaining properties into a new object
const { name: n, ...rest } = person;
// rest = { age: 25, city: 'NYC' }
// Useful for removing specific properties from an object

Function Parameters

// Destructure in parameters -- eliminates "options object" boilerplate
function greet({ name, age }) {
    return `Hello ${name}, you are ${age}`;
}

greet({ name: 'Alice', age: 25 });

// With defaults -- the = {} at the end is critical!
// Without it, calling createUser() with no arguments throws a TypeError
// because you cannot destructure undefined.
function createUser({ name = 'Guest', role = 'user' } = {}) {
    return { name, role };
}

createUser();                  // { name: 'Guest', role: 'user' }
createUser({ name: 'Alice' }); // { name: 'Alice', role: 'user' }
Common destructuring gotcha: Forgetting the = {} default on function parameters. If someone calls your function with no arguments, undefined gets destructured, which throws TypeError: Cannot destructure property 'name' of undefined. Always add = {} when the entire parameter object is optional.

Destructuring Edge Cases

// Edge case: destructuring null/undefined throws (unlike accessing a property)
const { a } = null;      // TypeError: Cannot destructure property 'a' of null
const { b } = undefined; // TypeError
// But property access on null/undefined can be guarded with optional chaining:
const val = null?.a;      // undefined (no error)

// Edge case: destructuring with computed keys
const key = 'name';
const { [key]: value } = { name: 'Alice' }; // value = 'Alice'

// Edge case: destructuring in for...of loops
const users = [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }];
for (const { name, age } of users) {
    console.log(`${name} is ${age}`); // Destructure each object in the array
}

// Edge case: mixing defaults and rename
const { name: userName = 'Anonymous', role: userRole = 'viewer' } = {};
// userName = 'Anonymous', userRole = 'viewer'

// Edge case: destructuring assignment to existing variables requires parentheses
let x, y;
({ x, y } = { x: 1, y: 2 }); // Without parens, { is treated as a block statement

2. Spread Operator

Expand iterables (arrays, strings) or objects.

Arrays

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// Combine arrays
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Copy array (shallow)
const copy = [...arr1];

// Convert iterable to array
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']

// Pass as function arguments
Math.max(...arr1); // 3

Objects (ES2018)

const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { theme: 'light' };

// Merge -- later properties override earlier ones (last writer wins)
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'en' }
// Order matters! { ...userPrefs, ...defaults } would keep theme: 'dark'

// Copy object (SHALLOW -- nested objects are shared by reference)
const copy = { ...defaults };

// Add/override properties -- a clean way to create modified copies
const updated = { ...defaults, debug: true };
Spread is shallow. This is the single most common source of bugs with spread/copy patterns. If your object contains nested objects or arrays, the spread creates new top-level properties but the nested values still point to the same references. Modifying a nested property in the copy modifies the original too. Use structuredClone() for deep copies.

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.
const name = 'Alice';
const age = 25;

// Interpolation -- any expression inside ${} is evaluated and converted to string
const greeting = `Hello, ${name}!`;

// Expressions -- not just variables, any valid JavaScript expression works
const message = `You will be ${age + 10} in 10 years`;
const status = `User is ${age >= 18 ? 'adult' : 'minor'}`;

// Multi-line strings -- preserves line breaks and indentation as-is
const html = `
    <div class="card">
        <h1>${name}</h1>
        <p>Age: ${age}</p>
    </div>
`;
// Note: the indentation becomes part of the string.
// Use .trim() if you need to remove leading/trailing whitespace.

// Tagged templates (advanced) -- a function that processes template literal parts
function highlight(strings, ...values) {
    // strings = ['Hello ', ', you are ', ''] (the static parts)
    // values = ['Alice', 25] (the interpolated expressions)
    return strings.reduce((result, str, i) => {
        const value = values[i] ? `<mark>${values[i]}</mark>` : '';
        return result + str + value;
    }, '');
}

const highlighted = highlight`Hello ${name}, you are ${age}`;
// 'Hello <mark>Alice</mark>, you are <mark>25</mark>'
// Tagged templates power libraries like styled-components and GraphQL's gql tag.

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

// utils.js -- each export is named and must be imported by that exact name
export const PI = 3.14159;

export function add(a, b) {
    return a + b;
}

export class Calculator {
    // ...
}

// main.js
import { PI, add, Calculator } from './utils.js';  // Must match export names
import { add as sum } from './utils.js';            // Rename on import
import * as utils from './utils.js';                // Import all as namespace object
// utils.PI, utils.add(), utils.Calculator

Default Exports

// User.js -- one default export per file (the "main" thing the module provides)
export default class User {
    constructor(name) {
        this.name = name;
    }
}

// main.js
import User from './User.js';              // Any name works (no curly braces)
import MyUser from './User.js';            // Same class, different local name
import User, { helper } from './User.js'; // Default + named in one import
Named vs Default exports — which to prefer? Named exports are generally better for maintainability: they enforce consistent names across your codebase, enable better IDE auto-imports, and make it obvious what a module provides. Default exports make sense for modules that export a single primary thing (a React component, a class). Many style guides (including the Airbnb style guide) prefer named exports.

ES Modules vs CommonJS — Complete Comparison

FeatureES Modules (import/export)CommonJS (require/module.exports)
Syntaximport { fn } from './mod.js'const { fn } = require('./mod')
LoadingStatic (analyzed at parse time)Dynamic (executed at runtime)
When evaluatedImports hoisted, executed in dependency orderExecuted where require() appears
Tree-shakingYes (bundlers can eliminate unused exports)No (entire module is loaded)
Conditional importsNo (use dynamic import() instead)Yes (if (x) require('./mod'))
Top-level awaitYes (ES2022)No
this at top levelundefinedmodule.exports
File extension.mjs or "type": "module" in package.json.cjs or default in Node.js
Browser supportNative (with type="module")Not supported (bundler required)
// Edge case: named exports are live bindings, not copies
// module.js
export let count = 0;
export function increment() { count++; }

// main.js
import { count, increment } from './module.js';
console.log(count); // 0
increment();
console.log(count); // 1 -- the value updated! It is a live binding.

// In CommonJS, this would NOT work -- require() copies the value:
// const { count } = require('./module'); // count is a snapshot, not a live binding

Dynamic Imports

Load modules on demand (code splitting). Unlike static import 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.
// Lazy load a heavy module only when the user actually needs it.
// The browser downloads heavy-module.js only when the button is clicked.
const button = document.getElementById('load');

button.addEventListener('click', async () => {
    const { heavyFunction } = await import('./heavy-module.js');
    heavyFunction();
});

// Practical example: loading a charting library only on the analytics page
if (window.location.pathname === '/analytics') {
    const { Chart } = await import('chart.js');
    new Chart(canvas, config);
}

5. Classes (ES6+)

Syntactic sugar over prototypes with additional features.
class Animal {
    // Private field (ES2022)
    #heartRate = 60;
    
    // Static field
    static kingdom = 'Animalia';
    
    constructor(name) {
        this.name = name;
    }
    
    // Getter
    get info() {
        return `${this.name} (${Animal.kingdom})`;
    }
    
    // Method
    speak() {
        console.log(`${this.name} makes a sound`);
    }
    
    // Private method (ES2022)
    #checkVitals() {
        return this.#heartRate > 0;
    }
    
    // Static method
    static create(name) {
        return new Animal(name);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    
    speak() {
        console.log(`${this.name} barks!`);
    }
}

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, even NaN.
const map = new Map();

// Any type as key -- this is the killer feature over plain objects
map.set('name', 'Alice');
map.set(1, 'one');           // Number key (objects would coerce to string '1')
map.set({ id: 1 }, 'object key'); // Object as key (compared by reference)

map.get('name');  // 'Alice'
map.has(1);       // true
map.size;         // 3 (unlike objects, Map tracks its own size)
map.delete('name');

// Iteration -- preserves insertion order (guaranteed, unlike objects before ES2015)
for (const [key, value] of map) {
    console.log(key, value);
}

Map vs Plain Object — Complete Comparison

FeatureMapPlain Object {}
Key typesAny type (objects, functions, numbers)Strings and Symbols only
Key orderInsertion order (guaranteed)Insertion order (mostly, with numeric key exceptions)
Sizemap.size (O(1))Object.keys(obj).length (O(n))
IterationDirectly iterable (for...of, .forEach)Need Object.keys/values/entries first
Performance (frequent add/delete)Optimized for thisNot optimized (hidden class transitions)
Prototype pollution riskNone (no inherited keys)Yes (toString, constructor, etc.)
JSON serializationNo (must convert manually)Yes (native JSON.stringify)
DestructuringNoYes (const { a, b } = obj)
Default choice forLookup tables with non-string keys, cachesConfig, API payloads, static data
// Edge case: numeric keys in objects are sorted first
const obj = { b: 2, 1: 'one', a: 1, 2: 'two' };
Object.keys(obj); // ['1', '2', 'b', 'a'] -- numeric keys first, then insertion order!

const map = new Map([['b', 2], [1, 'one'], ['a', 1], [2, 'two']]);
[...map.keys()]; // ['b', 1, 'a', 2] -- strict insertion order

// Edge case: Map keys use SameValueZero comparison (like === but NaN === NaN)
const map2 = new Map();
map2.set(NaN, 'not a number');
map2.get(NaN); // 'not a number' -- works! (NaN === NaN is false, but Map handles it)

Set (Unique Values)

A collection that automatically deduplicates. Every value can only appear once.
const set = new Set([1, 2, 2, 3, 3, 3]);
// Set(3) {1, 2, 3} -- duplicates are silently ignored

set.add(4);
set.has(2);    // true -- O(1) lookup, much faster than Array.includes()
set.delete(1);
set.size;      // 3

// The classic one-liner: remove duplicates from an array
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]

Set vs Array — When to Use Which

OperationSetArray
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
DuplicatesAutomatically preventedAllowed (must dedupe manually)
Index accessNot supportedarr[i] — O(1)
OrderingInsertion orderIndex-based order
.map(), .filter()Not directly (convert to array first)Native
JSON serializableNo (must convert to array)Yes
Best forUnique collections, membership testing, deduplicationOrdered lists, indexed access, transformation chains
// Set operations (not built-in, but easy to implement):
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);

// Union
const union = new Set([...a, ...b]); // {1, 2, 3, 4, 5, 6}

// Intersection
const intersection = new Set([...a].filter(x => b.has(x))); // {3, 4}

// Difference
const difference = new Set([...a].filter(x => !b.has(x))); // {1, 2}

// Edge case: Set uses SameValueZero (like Map), so NaN is handled correctly
const s = new Set([NaN, NaN]);
s.size; // 1 -- only one NaN (unlike array where indexOf(NaN) === -1)

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.
const weakMap = new WeakMap();
let obj = { data: 'secret' };

weakMap.set(obj, 'metadata');  // Only objects can be keys (not strings or numbers)
weakMap.get(obj); // 'metadata'

obj = null; // No more references to the object -- it can be garbage collected,
            // and the WeakMap entry is automatically cleaned up.

// Practical use case: storing private data for DOM elements or class instances
// without preventing them from being garbage collected.
// WeakMaps are NOT iterable (no .forEach, no .size) -- by design,
// since entries can disappear at any time.

Map vs WeakMap — When to Use Which

FeatureMapWeakMap
Key typesAny valueObjects only (no primitives)
Key retentionStrong (prevents garbage collection)Weak (allows garbage collection)
IterableYes (.forEach, for...of, .keys())No
.size propertyYesNo
Use caseGeneral-purpose key-value storeAssociating metadata with objects without causing memory leaks
// Practical WeakMap use case: caching expensive computations per object
const cache = new WeakMap();

function expensiveCompute(obj) {
    if (cache.has(obj)) return cache.get(obj);
    const result = /* expensive operation on obj */;
    cache.set(obj, result);
    return result;
}
// When 'obj' is garbage collected, its cache entry disappears automatically.
// With a regular Map, the cache would grow forever (memory leak).

// Practical WeakMap use case: truly private class data (pre-ES2022 #private)
const privates = new WeakMap();

class User {
    constructor(name, ssn) {
        this.name = name;
        privates.set(this, { ssn }); // SSN stored externally, keyed by instance
    }
    getSSN() {
        return privates.get(this).ssn;
    }
}
// No way to access SSN from outside without the WeakMap reference.
// When the User instance is garbage collected, the private data goes with it.

7. New Array & Object Methods

Array Methods (ES2019+)

// flat: Flatten nested arrays (depth argument controls how deep)
[[1, 2], [3, [4, 5]]].flat(2); // [1, 2, 3, 4, 5]
// flat(1) would give [1, 2, 3, [4, 5]] -- only one level deep

// flatMap: map then flat(1) in a single pass -- more efficient than .map().flat()
[1, 2].flatMap(x => [x, x * 2]); // [1, 2, 2, 4]

// at: Negative indexing -- finally, a clean way to get the last element!
const arr = [1, 2, 3, 4, 5];
arr.at(-1);  // 5 (last element) -- much cleaner than arr[arr.length - 1]
arr.at(-2);  // 4

// findLast / findLastIndex (ES2023) -- search from the end
arr.findLast(x => x % 2 === 0);      // 4 (last even number)
arr.findLastIndex(x => x % 2 === 0); // 3 (its index)

// toSorted, toReversed, toSpliced (ES2023) -- NON-MUTATING versions
// These return new arrays, leaving the original untouched.
// This is a big deal: .sort() and .reverse() mutate the original array,
// which is a classic source of bugs.
const sorted = arr.toSorted((a, b) => b - a); // [5, 4, 3, 2, 1]
const reversed = arr.toReversed();             // [5, 4, 3, 2, 1]
console.log(arr); // [1, 2, 3, 4, 5] -- original unchanged!

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 originalNon-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
// The classic .sort() mutation bug:
const original = [3, 1, 2];
const sorted = original.sort();     // [1, 2, 3]
console.log(original);             // [1, 2, 3] -- MUTATED! original === sorted
console.log(sorted === original);  // true -- they are the SAME array reference!

// Fix with toSorted (ES2023):
const safe = original.toSorted();  // New array, original untouched

// Fix without ES2023 (spread + sort):
const safeLegacy = [...original].sort(); // Copy first, then sort the copy

// Edge case: .sort() without a comparator sorts as STRINGS
[10, 9, 80].sort();                // [10, 80, 9] -- string comparison!
[10, 9, 80].sort((a, b) => a - b); // [9, 10, 80] -- numeric comparison

Object Methods (ES2017+)

const obj = { a: 1, b: 2, c: 3 };

// Object.entries / fromEntries
Object.entries(obj);              // [['a', 1], ['b', 2], ['c', 3]]
Object.fromEntries([['x', 10]]); // { x: 10 }

// Transform object
const doubled = Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [k, v * 2])
);
// { a: 2, b: 4, c: 6 }

// Object.hasOwn (ES2022) — Safer than hasOwnProperty
Object.hasOwn(obj, 'a'); // true

8. Other Modern Features

Optional Chaining & Nullish Coalescing

const user = { profile: { name: 'Alice' } };

// Optional chaining
user?.profile?.name;      // 'Alice'
user?.address?.city;      // undefined (no error)
user?.getName?.();        // undefined (function doesn't exist)

// Nullish coalescing
const value = null ?? 'default';  // 'default'
const zero = 0 ?? 'default';      // 0 (0 is not nullish)

Logical Assignment (ES2021)

These combine logical operators with assignment — a concise way to set defaults or update values conditionally.
let a = null;
let b = 'hello';

a ??= 'default'; // a = 'default' (was null -- nullish coalescing + assignment)
b ??= 'default'; // b = 'hello' (unchanged -- b is not null/undefined)

let x = 0;
x ||= 10;        // x = 10 (0 is falsy, so the assignment happens)
x &&= 20;        // x = 20 (x was truthy after the previous line)

// Practical use: setting defaults on config objects
options.timeout ??= 3000;    // Only set if timeout is null/undefined
options.retries ??= 3;

Numeric Separators (ES2021)

const billion = 1_000_000_000;
const bytes = 0xFF_FF_FF_FF;
const binary = 0b1010_0001;

String Methods (ES2017+)

// Padding
'5'.padStart(3, '0');  // '005'
'5'.padEnd(3, '0');    // '500'

// Trim
'  hello  '.trim();      // 'hello'
'  hello  '.trimStart(); // 'hello  '
'  hello  '.trimEnd();   // '  hello'

// replaceAll (ES2021)
'aabbcc'.replaceAll('a', 'x'); // 'xxbbcc'

// at (ES2022)
'hello'.at(-1); // 'o'

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 ?..
Next, we’ll learn about DOM & Browser APIs, how JavaScript interacts with web pages.

Interview Deep-Dive

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) or arr.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). Loses undefined values (they are stripped), converts Date objects to strings, chokes on circular references (throws), ignores Map, Set, Symbol keys, 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 mutates original.address.city because address is 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 either structuredClone or manual nested spreading: {...original, address: {...original.address, city: 'NYC'}}.
  • Production recommendation: use structuredClone for 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.
Follow-up: What does structuredClone NOT copy, and what happens if you try?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.
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, subsequent require() 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). import can load CJS modules (Node.js wraps them). But the semantics differ: CJS has module.exports as a single value, while ESM has named exports. When you import a CJS module, the entire module.exports becomes 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" in package.json (makes .js files ESM by default).
Follow-up: What is tree-shaking, and why does it only work with ES modules?Tree-shaking is a dead-code elimination technique where the bundler removes exported code that no consumer imports. It works with ES modules because 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%.
Strong Answer:
  • The core problem: memory leaks from strong references. If you store an object as a key in a Map or 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 a Foo instance 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 prevent obj from being collected.
  • WeakRef (ES2021): provides a weak reference to an object that you can dereference with .deref(). Returns undefined if the object has been garbage collected. Used in conjunction with FinalizationRegistry (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.
Follow-up: What is FinalizationRegistry and when would you pair it with WeakRef?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.
Strong Answer:
  • A tagged template literal is a function call where the function receives the template literal’s parts as arguments. The syntax is tagFunction\Hello name,age{name}, age `. 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.”
Follow-up: Can you write a simple ‘safe HTML’ tagged template that prevents XSS?
function safeHTML(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i - 1];
    const escaped = String(value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
    return result + escaped + str;
  });
}
Usage: 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.