JavaScript Crash Course – Lesson 6: Advanced Patterns and Best Practices

1. Closures

Closures are functions that have access to variables in their outer (enclosing) lexical scope, even after the outer function has returned.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.
JavaScript
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.
JavaScript
// 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:

JavaScript
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

  1. Implement a caching system using closures for a function that fetches user data from an API.
  2. Create a curried function for applying multiple filters to an array of objects.
  3. Refactor the weather app from previous lessons to use the module pattern and incorporate memoization for API calls.
  4. 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.