Modern JavaScript Patterns
Comprehensive guide for mastering modern JavaScript (ES6+) features, functional programming patterns, and best practices for writing clean, maintainable, and performant code.
When to Use This Skill
-
Refactoring legacy JavaScript to modern syntax
-
Implementing functional programming patterns
-
Optimizing JavaScript performance
-
Writing maintainable and readable code
-
Working with asynchronous operations
-
Building modern web applications
-
Migrating from callbacks to Promises/async-await
-
Implementing data transformation pipelines
ES6+ Core Features
- Arrow Functions
Syntax and Use Cases:
// Traditional function function add(a, b) { return a + b; }
// Arrow function const add = (a, b) => a + b;
// Single parameter (parentheses optional) const double = (x) => x * 2;
// No parameters const getRandom = () => Math.random();
// Multiple statements (need curly braces) const processUser = (user) => { const normalized = user.name.toLowerCase(); return { ...user, name: normalized }; };
// Returning objects (wrap in parentheses) const createUser = (name, age) => ({ name, age });
Lexical 'this' Binding:
class Counter { constructor() { this.count = 0; }
// Arrow function preserves 'this' context increment = () => { this.count++; };
// Traditional function loses 'this' in callbacks incrementTraditional() { setTimeout(function () { this.count++; // 'this' is undefined }, 1000); }
// Arrow function maintains 'this' incrementArrow() { setTimeout(() => { this.count++; // 'this' refers to Counter instance }, 1000); } }
- Destructuring
Object Destructuring:
const user = { id: 1, name: "John Doe", email: "john@example.com", address: { city: "New York", country: "USA", }, };
// Basic destructuring const { name, email } = user;
// Rename variables const { name: userName, email: userEmail } = user;
// Default values const { age = 25 } = user;
// Nested destructuring const { address: { city, country }, } = user;
// Rest operator const { id, ...userWithoutId } = user;
// Function parameters
function greet({ name, age = 18 }) {
console.log(Hello ${name}, you are ${age});
}
greet(user);
Array Destructuring:
const numbers = [1, 2, 3, 4, 5];
// Basic destructuring const [first, second] = numbers;
// Skip elements const [, , third] = numbers;
// Rest operator const [head, ...tail] = numbers;
// Swapping variables let a = 1, b = 2; [a, b] = [b, a];
// Function return values function getCoordinates() { return [10, 20]; } const [x, y] = getCoordinates();
// Default values const [one, two, three = 0] = [1, 2];
- Spread and Rest Operators
Spread Operator:
// Array spreading const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const combined = [...arr1, ...arr2];
// Object spreading const defaults = { theme: "dark", lang: "en" }; const userPrefs = { theme: "light" }; const settings = { ...defaults, ...userPrefs };
// Function arguments const numbers = [1, 2, 3]; Math.max(...numbers);
// Copying arrays/objects (shallow copy) const copy = [...arr1]; const objCopy = { ...user };
// Adding items immutably const newArr = [...arr1, 4, 5]; const newObj = { ...user, age: 30 };
Rest Parameters:
// Collect function arguments function sum(...numbers) { return numbers.reduce((total, num) => total + num, 0); } sum(1, 2, 3, 4, 5);
// With regular parameters
function greet(greeting, ...names) {
return ${greeting} ${names.join(", ")};
}
greet("Hello", "John", "Jane", "Bob");
// Object rest const { id, ...userData } = user;
// Array rest const [first, ...rest] = [1, 2, 3, 4, 5];
- Template Literals
// Basic usage
const name = "John";
const greeting = Hello, ${name}!;
// Multi-line strings
const html = <div> <h1>${title}</h1> <p>${content}</p> </div>;
// Expression evaluation
const price = 19.99;
const total = Total: $${(price * 1.2).toFixed(2)};
// Tagged template literals
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] || "";
return result + str + <mark>${value}</mark>;
}, "");
}
const name = "John";
const age = 30;
const html = highlightName: ${name}, Age: ${age};
// Output: "Name: <mark>John</mark>, Age: <mark>30</mark>"
- Enhanced Object Literals
const name = "John"; const age = 30;
// Shorthand property names const user = { name, age };
// Shorthand method names const calculator = { add(a, b) { return a + b; }, subtract(a, b) { return a - b; }, };
// Computed property names
const field = "email";
const user = {
name: "John",
[field]: "john@example.com",
get${field.charAt(0).toUpperCase()}${field.slice(1)} {
return this[field];
},
};
// Dynamic property creation const createUser = (name, ...props) => { return props.reduce( (user, [key, value]) => ({ ...user, [key]: value, }), { name }, ); };
const user = createUser("John", ["age", 30], ["email", "john@example.com"]);
Asynchronous Patterns
- Promises
Creating and Using Promises:
// Creating a promise const fetchUser = (id) => { return new Promise((resolve, reject) => { setTimeout(() => { if (id > 0) { resolve({ id, name: "John" }); } else { reject(new Error("Invalid ID")); } }, 1000); }); };
// Using promises fetchUser(1) .then((user) => console.log(user)) .catch((error) => console.error(error)) .finally(() => console.log("Done"));
// Chaining promises fetchUser(1) .then((user) => fetchUserPosts(user.id)) .then((posts) => processPosts(posts)) .then((result) => console.log(result)) .catch((error) => console.error(error));
Promise Combinators:
// Promise.all - Wait for all promises const promises = [fetchUser(1), fetchUser(2), fetchUser(3)];
Promise.all(promises) .then((users) => console.log(users)) .catch((error) => console.error("At least one failed:", error));
// Promise.allSettled - Wait for all, regardless of outcome Promise.allSettled(promises).then((results) => { results.forEach((result) => { if (result.status === "fulfilled") { console.log("Success:", result.value); } else { console.log("Error:", result.reason); } }); });
// Promise.race - First to complete Promise.race(promises) .then((winner) => console.log("First:", winner)) .catch((error) => console.error(error));
// Promise.any - First to succeed Promise.any(promises) .then((first) => console.log("First success:", first)) .catch((error) => console.error("All failed:", error));
- Async/Await
Basic Usage:
// Async function always returns a Promise
async function fetchUser(id) {
const response = await fetch(/api/users/${id});
const user = await response.json();
return user;
}
// Error handling with try/catch async function getUserData(id) { try { const user = await fetchUser(id); const posts = await fetchUserPosts(user.id); return { user, posts }; } catch (error) { console.error("Error fetching data:", error); throw error; } }
// Sequential vs Parallel execution async function sequential() { const user1 = await fetchUser(1); // Wait const user2 = await fetchUser(2); // Then wait return [user1, user2]; }
async function parallel() { const [user1, user2] = await Promise.all([fetchUser(1), fetchUser(2)]); return [user1, user2]; }
Advanced Patterns:
// Async IIFE (async () => { const result = await someAsyncOperation(); console.log(result); })();
// Async iteration async function processUsers(userIds) { for (const id of userIds) { const user = await fetchUser(id); await processUser(user); } }
// Top-level await (ES2022) const config = await fetch("/config.json").then((r) => r.json());
// Retry logic async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { try { return await fetch(url); } catch (error) { if (i === retries - 1) throw error; await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); } } }
// Timeout wrapper async function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms), ); return Promise.race([promise, timeout]); }
Functional Programming Patterns
- Array Methods
Map, Filter, Reduce:
const users = [ { id: 1, name: "John", age: 30, active: true }, { id: 2, name: "Jane", age: 25, active: false }, { id: 3, name: "Bob", age: 35, active: true }, ];
// Map - Transform array const names = users.map((user) => user.name); const upperNames = users.map((user) => user.name.toUpperCase());
// Filter - Select elements const activeUsers = users.filter((user) => user.active); const adults = users.filter((user) => user.age >= 18);
// Reduce - Aggregate data const totalAge = users.reduce((sum, user) => sum + user.age, 0); const avgAge = totalAge / users.length;
// Group by property const byActive = users.reduce((groups, user) => { const key = user.active ? "active" : "inactive"; return { ...groups, [key]: [...(groups[key] || []), user], }; }, {});
// Chaining methods const result = users .filter((user) => user.active) .map((user) => user.name) .sort() .join(", ");
Advanced Array Methods:
// Find - First matching element const user = users.find((u) => u.id === 2);
// FindIndex - Index of first match const index = users.findIndex((u) => u.name === "Jane");
// Some - At least one matches const hasActive = users.some((u) => u.active);
// Every - All match const allAdults = users.every((u) => u.age >= 18);
// FlatMap - Map and flatten const userTags = [ { name: "John", tags: ["admin", "user"] }, { name: "Jane", tags: ["user"] }, ]; const allTags = userTags.flatMap((u) => u.tags);
// From - Create array from iterable const str = "hello"; const chars = Array.from(str); const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
// Of - Create array from arguments const arr = Array.of(1, 2, 3);
- Higher-Order Functions
Functions as Arguments:
// Custom forEach function forEach(array, callback) { for (let i = 0; i < array.length; i++) { callback(array[i], i, array); } }
// Custom map function map(array, transform) { const result = []; for (const item of array) { result.push(transform(item)); } return result; }
// Custom filter function filter(array, predicate) { const result = []; for (const item of array) { if (predicate(item)) { result.push(item); } } return result; }
Functions Returning Functions:
// Currying const multiply = (a) => (b) => a * b; const double = multiply(2); const triple = multiply(3);
console.log(double(5)); // 10 console.log(triple(5)); // 15
// Partial application function partial(fn, ...args) { return (...moreArgs) => fn(...args, ...moreArgs); }
const add = (a, b, c) => a + b + c; const add5 = partial(add, 5); console.log(add5(3, 2)); // 10
// Memoization function memoize(fn) { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }; }
const fibonacci = memoize((n) => { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); });
- Composition and Piping
// Function composition const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
// Example usage const addOne = (x) => x + 1; const double = (x) => x * 2; const square = (x) => x * x;
const composed = compose(square, double, addOne); console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
const piped = pipe(addOne, double, square); console.log(piped(3)); // ((3 + 1) * 2)^2 = 64
// Practical example const processUser = pipe( (user) => ({ ...user, name: user.name.trim() }), (user) => ({ ...user, email: user.email.toLowerCase() }), (user) => ({ ...user, age: parseInt(user.age) }), );
const user = processUser({ name: " John ", email: "JOHN@EXAMPLE.COM", age: "30", });
- Pure Functions and Immutability
// Impure function (modifies input) function addItemImpure(cart, item) { cart.items.push(item); cart.total += item.price; return cart; }
// Pure function (no side effects) function addItemPure(cart, item) { return { ...cart, items: [...cart.items, item], total: cart.total + item.price, }; }
// Immutable array operations const numbers = [1, 2, 3, 4, 5];
// Add to array const withSix = [...numbers, 6];
// Remove from array const withoutThree = numbers.filter((n) => n !== 3);
// Update array element const doubled = numbers.map((n) => (n === 3 ? n * 2 : n));
// Immutable object operations const user = { name: "John", age: 30 };
// Update property const olderUser = { ...user, age: 31 };
// Add property const withEmail = { ...user, email: "john@example.com" };
// Remove property const { age, ...withoutAge } = user;
// Deep cloning (simple approach) const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
// Better deep cloning const structuredClone = (obj) => globalThis.structuredClone(obj);
Modern Class Features
// Class syntax class User { // Private fields #password;
// Public fields id; name;
// Static field static count = 0;
constructor(id, name, password) { this.id = id; this.name = name; this.#password = password; User.count++; }
// Public method
greet() {
return Hello, ${this.name};
}
// Private method
#hashPassword(password) {
return hashed_${password};
}
// Getter get displayName() { return this.name.toUpperCase(); }
// Setter set password(newPassword) { this.#password = this.#hashPassword(newPassword); }
// Static method static create(id, name, password) { return new User(id, name, password); } }
// Inheritance class Admin extends User { constructor(id, name, password, role) { super(id, name, password); this.role = role; }
greet() {
return ${super.greet()}, I'm an admin;
}
}
Modules (ES6)
// Exporting // math.js export const PI = 3.14159; export function add(a, b) { return a + b; } export class Calculator { // ... }
// Default export export default function multiply(a, b) { return a * b; }
// Importing // app.js import multiply, { PI, add, Calculator } from "./math.js";
// Rename imports import { add as sum } from "./math.js";
// Import all import * as Math from "./math.js";
// Dynamic imports const module = await import("./math.js"); const { add } = await import("./math.js");
// Conditional loading if (condition) { const module = await import("./feature.js"); module.init(); }
Iterators and Generators
// Custom iterator const range = { from: 1, to: 5,
Symbol.iterator { return { current: this.from, last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
}, };
for (const num of range) { console.log(num); // 1, 2, 3, 4, 5 }
// Generator function function* rangeGenerator(from, to) { for (let i = from; i <= to; i++) { yield i; } }
for (const num of rangeGenerator(1, 5)) { console.log(num); }
// Infinite generator function* fibonacci() { let [prev, curr] = [0, 1]; while (true) { yield curr; [prev, curr] = [curr, prev + curr]; } }
// Async generator
async function* fetchPages(url) {
let page = 1;
while (true) {
const response = await fetch(${url}?page=${page});
const data = await response.json();
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const page of fetchPages("/api/users")) { console.log(page); }
Modern Operators
// Optional chaining const user = { name: "John", address: { city: "NYC" } }; const city = user?.address?.city; const zipCode = user?.address?.zipCode; // undefined
// Function call const result = obj.method?.();
// Array access const first = arr?.[0];
// Nullish coalescing const value = null ?? "default"; // 'default' const value = undefined ?? "default"; // 'default' const value = 0 ?? "default"; // 0 (not 'default') const value = "" ?? "default"; // '' (not 'default')
// Logical assignment let a = null; a ??= "default"; // a = 'default'
let b = 5; b ??= 10; // b = 5 (unchanged)
let obj = { count: 0 }; obj.count ||= 1; // obj.count = 1 obj.count &&= 2; // obj.count = 2
Performance Optimization
// Debounce function debounce(fn, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }
const searchDebounced = debounce(search, 300);
// Throttle function throttle(fn, limit) { let inThrottle; return (...args) => { if (!inThrottle) { fn(...args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; }
const scrollThrottled = throttle(handleScroll, 100);
// Lazy evaluation function* lazyMap(iterable, transform) { for (const item of iterable) { yield transform(item); } }
// Use only what you need const numbers = [1, 2, 3, 4, 5]; const doubled = lazyMap(numbers, (x) => x * 2); const first = doubled.next().value; // Only computes first value
Best Practices
-
Use const by default: Only use let when reassignment is needed
-
Prefer arrow functions: Especially for callbacks
-
Use template literals: Instead of string concatenation
-
Destructure objects and arrays: For cleaner code
-
Use async/await: Instead of Promise chains
-
Avoid mutating data: Use spread operator and array methods
-
Use optional chaining: Prevent "Cannot read property of undefined"
-
Use nullish coalescing: For default values
-
Prefer array methods: Over traditional loops
-
Use modules: For better code organization
-
Write pure functions: Easier to test and reason about
-
Use meaningful variable names: Self-documenting code
-
Keep functions small: Single responsibility principle
-
Handle errors properly: Use try/catch with async/await
-
Use strict mode: 'use strict' for better error catching
Common Pitfalls
-
this binding confusion: Use arrow functions or bind()
-
Async/await without error handling: Always use try/catch
-
Promise creation unnecessary: Don't wrap already async functions
-
Mutation of objects: Use spread operator or Object.assign()
-
Forgetting await: Async functions return promises
-
Blocking event loop: Avoid synchronous operations
-
Memory leaks: Clean up event listeners and timers
-
Not handling promise rejections: Use catch() or try/catch