The Modern JavaScript Revolution#
ES6 (ECMAScript 2015) was the biggest single update to JavaScript. Subsequent yearly releases (ES2016–ES2024) added incremental improvements. This file covers the features that fundamentally changed how JavaScript is written.
Destructuring#
Destructuring extracts values from objects and arrays into distinct variables using a pattern that mirrors the data structure.
Object Destructuring#
const user = { name: "Alice", age: 30, city: "Barcelona", role: "admin" };
// Basic
const { name, age } = user;
// Rename
const { name: userName, age: userAge } = user;
// Default values
const { name, country = "Unknown" } = user; // country = "Unknown"
// Nested
const response = {
data: {
user: { id: 1, profile: { avatar: "url" } }
}
};
const { data: { user: { profile: { avatar } } } } = response;
// Rest (collect remaining)
const { name, ...rest } = user; // rest = { age: 30, city: "Barcelona", role: "admin" }
// Computed property names
const key = "name";
const { [key]: value } = user; // value = "Alice"
Array Destructuring#
const coords = [41.3851, 2.1734, 12];
// Basic
const [lat, lng] = coords;
// Skip elements
const [, , altitude] = coords;
// Default values
const [x, y, z, w = 0] = coords;
// Rest
const [first, ...remaining] = [1, 2, 3, 4, 5]; // remaining = [2, 3, 4, 5]
// Swap variables (no temp needed)
let a = 1, b = 2;
[a, b] = [b, a]; // a = 2, b = 1
Function Parameter Destructuring#
// Object parameters with defaults
function createUser({ name, age, role = "user", active = true } = {}) {
return { name, age, role, active, createdAt: new Date() };
}
createUser({ name: "Alice", age: 30 });
createUser(); // uses all defaults (the = {} handles undefined argument)
// Array parameters
function head([first, ...rest]) {
return { first, rest };
}
head([1, 2, 3]); // { first: 1, rest: [2, 3] }
Spread and Rest Operators#
The ... syntax serves two purposes depending on context:
- Spread — expands an iterable into individual elements
- Rest — collects multiple elements into a single array/object
Spread#
// Array spread
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const copy = [...arr1]; // shallow copy
const withInsert = [0, ...arr1, 4]; // [0, 1, 2, 3, 4]
// Object spread (shallow merge)
const defaults = { theme: "dark", lang: "en", fontSize: 14 };
const userPrefs = { theme: "light", fontSize: 16 };
const config = { ...defaults, ...userPrefs };
// { theme: "light", lang: "en", fontSize: 16 } — later spreads win
// Function arguments
const numbers = [5, 2, 8, 1, 9];
Math.max(...numbers); // 9 (equivalent to Math.max(5, 2, 8, 1, 9))
Rest#
// Function parameters
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4); // 10
// With leading params
function log(level, ...messages) {
console.log(`[${level}]`, ...messages);
}
log("ERROR", "Connection failed", "retrying in 5s");Immutable Update Patterns#
// Add to array (immutable)
const addItem = (arr, item) => [...arr, item];
// Remove from array (immutable)
const removeAt = (arr, index) => [...arr.slice(0, index), ...arr.slice(index + 1)];
// Update object property (immutable)
const updateUser = (user, updates) => ({ ...user, ...updates });
// Deep update (nested)
const updateAddress = (user, city) => ({
...user,
address: { ...user.address, city }
});Template Literals#
// Basic interpolation
const name = "Alice";
const greeting = `Hello, ${name}!`; // "Hello, Alice!"
// Expressions
const price = 19.99;
const tax = 0.21;
const total = `Total: €${(price * (1 + tax)).toFixed(2)}`; // "Total: €24.19"
// Multi-line (preserves whitespace)
const html = `
<div class="card">
<h2>${title}</h2>
<p>${description}</p>
</div>
`;
// Tagged templates (custom processing)
function sql(strings, ...values) {
// strings: ["SELECT * FROM users WHERE id = ", " AND active = ", ""]
// values: [userId, true]
return {
text: strings.join("$" + strings.map((_, i) => i + 1).join("$")),
values: values
};
}
const query = sql`SELECT * FROM users WHERE id = ${userId} AND active = ${true}`;
// Parameterized query — safe from SQL injection
// Highlight syntax (common in CSS-in-JS)
const styled = css`
color: ${theme.primary};
font-size: ${fontSize}px;
`;Iterators and Generators#
The Iterator Protocol#
Any object with a [Symbol.iterator]() method that returns { next() → { value, done } } is iterable.
// Custom iterable
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { done: true };
}
};
}
}
const range = new Range(1, 10, 2);
for (const n of range) console.log(n); // 1, 3, 5, 7, 9
[...range]; // [1, 3, 5, 7, 9] — spread works on iterables
Generators#
Generators are functions that can pause and resume, yielding multiple values lazily.
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Lazy — only computes on demand
const fib = fibonacci();
fib.next(); // { value: 0, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 2, done: false }
// Take first N values
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
[...take(fibonacci(), 8)]; // [0, 1, 1, 2, 3, 5, 8, 13]
// Generator delegation
function* concat(...iterables) {
for (const iter of iterables) {
yield* iter; // delegates to another iterable
}
}
[...concat([1, 2], [3, 4], [5])]; // [1, 2, 3, 4, 5]
Async Generators#
async function* fetchPages(url) {
let nextUrl = url;
while (nextUrl) {
const response = await fetch(nextUrl);
const data = await response.json();
yield data.items;
nextUrl = data.nextPage;
}
}
// Consume with for-await-of
for await (const page of fetchPages("/api/users?page=1")) {
for (const user of page) {
processUser(user);
}
}Symbols#
Symbols are unique, immutable identifiers. Their primary use is as non-colliding property keys.
// Creating symbols
const id = Symbol("id"); // description is for debugging only
const id2 = Symbol("id");
id === id2; // false — every Symbol is unique
// As property keys (won't collide with string keys)
const user = {
name: "Alice",
[id]: 12345 // symbol-keyed property
};
user[id]; // 12345
Object.keys(user); // ["name"] — symbols are not enumerable
// Well-known symbols (customize language behavior)
class MyArray {
[Symbol.iterator]() { /* ... */ } // make iterable
[Symbol.toPrimitive](hint) { /* ... */ } // customize coercion
get [Symbol.toStringTag]() { return "MyArray"; } // customize toString
}
// Global symbol registry (shared across realms)
const shared = Symbol.for("app.config"); // creates or retrieves
Symbol.for("app.config") === shared; // true (same reference)
Proxy and Reflect#
Proxies intercept fundamental operations on objects (get, set, delete, etc.).
const handler = {
get(target, prop, receiver) {
console.log(`Accessing ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (prop === "age" && (value < 0 || value > 150)) {
throw new RangeError("Invalid age");
}
return Reflect.set(target, prop, value, receiver);
}
};
const user = new Proxy({ name: "Alice", age: 30 }, handler);
user.name; // logs "Accessing name", returns "Alice"
user.age = 200; // throws RangeError
// Practical: reactive data (Vue 3 uses this internally)
function reactive(obj) {
return new Proxy(obj, {
set(target, prop, value) {
const oldValue = target[prop];
target[prop] = value;
if (oldValue !== value) {
notify(prop, value); // trigger UI update
}
return true;
}
});
}WeakRef and FinalizationRegistry#
// WeakRef — holds reference without preventing garbage collection
let obj = { data: "important" };
const weakRef = new WeakRef(obj);
weakRef.deref(); // { data: "important" }
obj = null; // original reference gone
// After GC: weakRef.deref() → undefined
// FinalizationRegistry — callback when object is GC'd
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with key ${heldValue} was garbage collected`);
});
let cache = { key: "value" };
registry.register(cache, "cache-key");
cache = null; // eventually logs the message
Key Takeaways#
- Destructuring eliminates verbose property access — use it in function parameters for self-documenting APIs
- Spread creates shallow copies — nested objects are still shared references
- Generators enable lazy evaluation — process infinite sequences without memory issues
- Symbols guarantee uniqueness — use for metadata keys that won’t collide with user data
- Proxies intercept operations — foundation for reactivity systems, validation, and logging
- Tagged templates — powerful for DSLs (SQL, CSS, GraphQL) with built-in safety
for...ofworks on any iterable — arrays, strings, Maps, Sets, generators, custom iterables