1. Closures
Closures are functions that have access to variables in their outer (enclosing) lexical scope, even after the outer function has returned.
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
2. Higher-Order Functions
Higher-order functions are functions that take other functions as arguments or return functions.
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
3. Currying
Currying is the technique of translating a function that takes multiple arguments into a sequence of functions, each with a single argument.
const multiply = (a) => (b) => a * b;
const double = multiply(2);
console.log(double(5)); // 10
console.log(multiply(3)(4)); // 12
4. Memoization
Memoization is an optimization technique that speeds up applications by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
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;
};
}
const expensiveFunction = memoize((n) => {
console.log('Computing...');
return n * 2;
});
console.log(expensiveFunction(5)); // Computing... 10
console.log(expensiveFunction(5)); // 10 (cached)
5. The Module Pattern
The module pattern is a design pattern used to wrap a set of variables and functions together in a single scope, providing privacy and organization.
const Calculator = (function() {
let result = 0;
function add(a, b) {
result = a + b;
}
function getResult() {
return result;
}
return {
add: add,
getResult: getResult
};
})();
Calculator.add(5, 3);
console.log(Calculator.getResult()); // 8
6. Promises and Async/Await Best Practices
When working with asynchronous code, follow these best practices:
- Always handle errors in promises and async functions.
- Use Promise.all for concurrent asynchronous operations.
- Avoid mixing callbacks and promises.
async function fetchUserData(userId) {
try {
const [user, posts] = await Promise.all([
fetch(`/api/users/${userId}`).then(res => res.json()),
fetch(`/api/users/${userId}/posts`).then(res => res.json())
]);
return { user, posts };
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
}
7. Clean Code Principles
- Use meaningful and pronounceable variable names.
- Functions should do one thing.
- Don’t use flags as function parameters.
- Avoid side effects.
- Use early returns to reduce nesting.
// Bad
function createUserIfValid(u, a) {
if (u.length >= 3 && a >= 18) {
// create user
}
}
// Good
function createUser(user) {
if (isUserValid(user)) {
// create user
}
}
function isUserValid(user) {
return user.name.length >= 3 && user.age >= 18;
}
Practical Example: Task Manager
Let’s apply some of these patterns and best practices to create a simple task manager:
const TaskManager = (function() {
const tasks = new Map();
function addTask(id, description) {
if (!description) throw new Error('Task description is required');
tasks.set(id, { id, description, completed: false });
}
function completeTask(id) {
if (!tasks.has(id)) throw new Error('Task not found');
const task = tasks.get(id);
tasks.set(id, { ...task, completed: true });
}
function deleteTask(id) {
if (!tasks.has(id)) throw new Error('Task not found');
tasks.delete(id);
}
const getTasks = memoize(() => Array.from(tasks.values()));
return { addTask, completeTask, deleteTask, getTasks };
})();
// Usage
TaskManager.addTask(1, 'Learn JavaScript');
TaskManager.addTask(2, 'Practice coding');
TaskManager.completeTask(1);
console.log(TaskManager.getTasks());
Practice Exercise
- Implement a caching system using closures for a function that fetches user data from an API.
- Create a curried function for applying multiple filters to an array of objects.
- Refactor the weather app from previous lessons to use the module pattern and incorporate memoization for API calls.
- Write a higher-order function that adds logging functionality to any given function without modifying the original function.
Conclusion
This lesson covered advanced JavaScript patterns and best practices that will help you write more efficient, maintainable, and professional code. These concepts are widely used in large-scale JavaScript applications and libraries. Practice incorporating these patterns into your projects to become a more proficient JavaScript developer.
In the next lesson, we’ll explore testing in JavaScript, covering unit testing and test-driven development (TDD) principles.