JavaScript has evolved dramatically since ES6 (ES2015), introducing powerful features that have transformed how we write modern web applications. Understanding these features and patterns is essential for writing clean, maintainable, and efficient code.
ES6+ Core Features
1. Destructuring Assignment
Destructuring allows you to extract values from objects and arrays into distinct variables.
// Object destructuring
const user = {
name: 'John Doe',
email: 'john@example.com',
age: 30,
address: {
city: 'New York',
country: 'USA'
}
};
// Basic destructuring
const { name, email, age } = user;
// Nested destructuring
const { address: { city, country } } = user;
// With default values
const { name, role = 'user' } = user;
// Array destructuring
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a];
2. Template Literals
Template literals provide an elegant way to create strings with embedded expressions.
// Basic template literals
const name = 'John';
const greeting = `Hello, ${name}!`;
// Multi-line strings
const html = `
${user.name}
${user.email}
`;
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i - 1] || '';
return result + str + (value ? `${value}` : '');
});
}
const message = highlight`Hello ${name}, welcome to ${site}!`;
3. Arrow Functions
Arrow functions provide a concise syntax for writing function expressions.
// Basic arrow functions
const add = (a, b) => a + b;
const multiply = (a, b) => {
const result = a * b;
return result;
};
// Array methods with arrow functions
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
// Object methods
const calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b
};
Advanced Async Patterns
1. Async/Await
Async/await provides a more readable way to work with promises.
// Traditional promise chain
fetch('/api/users')
.then(response => response.json())
.then(users => {
return fetch(`/api/users/${users[0].id}/posts`);
})
.then(response => response.json())
.then(posts => {
console.log(posts);
})
.catch(error => {
console.error('Error:', error);
});
// Async/await equivalent
async function fetchUserPosts() {
try {
const response = await fetch('/api/users');
const users = await response.json();
const postsResponse = await fetch(`/api/users/${users[0].id}/posts`);
const posts = await postsResponse.json();
console.log(posts);
} catch (error) {
console.error('Error:', error);
}
}
// Parallel execution
async function fetchMultipleUsers(userIds) {
const promises = userIds.map(id =>
fetch(`/api/users/${id}`).then(res => res.json())
);
const users = await Promise.all(promises);
return users;
}
2. Promise Patterns
Understanding advanced promise patterns for better error handling and performance.
// Promise.allSettled - Wait for all promises to complete
async function fetchUserData(userIds) {
const promises = userIds.map(id =>
fetch(`/api/users/${id}`)
.then(res => res.json())
.catch(error => ({ error, id }))
);
const results = await Promise.allSettled(promises);
return results.map(result =>
result.status === 'fulfilled' ? result.value : result.reason
);
}
// Promise.race - First to complete wins
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw new Error('Request timeout');
}
}
ES6 Modules
1. Module Syntax
ES6 modules provide a standardized way to organize and share code.
// math.js - Named exports
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// utils.js - Default export
const formatCurrency = (amount, currency = 'USD') => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
};
export default formatCurrency;
// main.js - Importing modules
import { add, multiply } from './math.js';
import formatCurrency from './utils.js';
// Dynamic imports
async function loadModule(moduleName) {
const module = await import(`./modules/${moduleName}.js`);
return module.default;
}
2. Module Patterns
// Singleton pattern with modules
let instance = null;
class Database {
constructor() {
if (instance) {
return instance;
}
instance = this;
this.connection = null;
}
async connect() {
if (!this.connection) {
this.connection = await createConnection();
}
return this.connection;
}
}
export default new Database();
// Factory pattern
export class UserFactory {
static createUser(type, data) {
switch (type) {
case 'admin':
return new AdminUser(data);
case 'regular':
return new RegularUser(data);
default:
throw new Error(`Unknown user type: ${type}`);
}
}
}
Functional Programming Patterns
1. Higher-Order Functions
Functions that take other functions as arguments or return functions.
// 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 multiplyByTwo = x => x * 2;
const square = x => x ** 2;
const transform = pipe(addOne, multiplyByTwo, square);
console.log(transform(3)); // 64
// Currying
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
};
const add = curry((a, b, c) => a + b + c);
const addFive = add(5);
const addFiveAndThree = addFive(3);
console.log(addFiveAndThree(2)); // 10
2. Immutability Patterns
// Immutable object updates
const updateUser = (user, updates) => ({
...user,
...updates
});
const addPost = (user, post) => ({
...user,
posts: [...user.posts, post]
});
// Immutable array operations
const addItem = (array, item) => [...array, item];
const removeItem = (array, index) =>
array.filter((_, i) => i !== index);
const updateItem = (array, index, updates) =>
array.map((item, i) => i === index ? { ...item, ...updates } : item);
// Deep cloning
const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item));
}
if (typeof obj === 'object') {
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
};
Decorators and Metadata
1. Property Decorators
Decorators provide a way to add metadata and modify behavior.
// Property decorator
function readonly(target, propertyName) {
Object.defineProperty(target, propertyName, {
writable: false,
configurable: false
});
}
function validate(target, propertyName, descriptor) {
const originalSet = descriptor.set;
descriptor.set = function(value) {
if (typeof value !== 'string' || value.length < 3) {
throw new Error(`${propertyName} must be a string with at least 3 characters`);
}
originalSet.call(this, value);
};
}
class User {
@readonly
id = 1;
@validate
set name(value) {
this._name = value;
}
get name() {
return this._name;
}
}
2. Method Decorators
// Logging decorator
function log(target, propertyName, descriptor) {
const method = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${propertyName} with:`, args);
const result = method.apply(this, args);
console.log(`${propertyName} returned:`, result);
return result;
};
}
// Cache decorator
function cache(target, propertyName, descriptor) {
const method = descriptor.value;
const cacheMap = new Map();
descriptor.value = function(...args) {
const key = JSON.stringify(args);
if (cacheMap.has(key)) {
return cacheMap.get(key);
}
const result = method.apply(this, args);
cacheMap.set(key, result);
return result;
};
}
class Calculator {
@log
@cache
fibonacci(n) {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Modern JavaScript Best Practices
1. Error Handling
// Custom error classes
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
// Error handling patterns
async function handleApiCall() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new NetworkError('API request failed', response.status);
}
const data = await response.json();
if (!data.valid) {
throw new ValidationError('Invalid data received', 'data');
}
return data;
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation error in ${error.field}:`, error.message);
} else if (error instanceof NetworkError) {
console.error(`Network error (${error.status}):`, error.message);
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
2. Performance Optimization
// Debouncing
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Throttling
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
Conclusion
Modern JavaScript provides powerful features that enable us to write more expressive, maintainable, and efficient code. By mastering these patterns and features, you can create robust applications that are easier to debug, test, and scale.
Remember that while these features are powerful, they should be used judiciously. Always consider readability, performance, and browser compatibility when choosing which features to use in your projects.