JavaScript Advanced (Part 2)

Master advanced JavaScript concepts including asynchronous programming, modules, design patterns, and testing.

JavaScript Advanced Content

This content has been divided into two parts for better readability:

Asynchronous JavaScript

Asynchronous programming is a fundamental concept in JavaScript, especially for web development where operations like network requests, file I/O, and timers are common.

The Event Loop

The event loop is the mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded.

console.log('Start');

setTimeout(() => {
    console.log('Timeout callback');
}, 0);

Promise.resolve().then(() => {
    console.log('Promise callback');
});

console.log('End');

// Output:
// Start
// End
// Promise callback
// Timeout callback

The event loop consists of several key components:

console.log('Script start');

setTimeout(() => {
    console.log('setTimeout 1');
}, 0);

setTimeout(() => {
    console.log('setTimeout 2');
}, 0);

Promise.resolve()
    .then(() => {
        console.log('Promise 1');
        return Promise.resolve();
    })
    .then(() => {
        console.log('Promise 2');
    });

console.log('Script end');

// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout 1
// setTimeout 2

Callbacks

Callbacks are functions passed as arguments to other functions, to be executed after an operation completes.

// Simple callback example
function fetchData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: 'John' };
        callback(null, data);
    }, 1000);
}

fetchData((error, data) => {
    if (error) {
        console.error('Error:', error);
        return;
    }
    console.log('Data:', data);
});

// Callback hell (nested callbacks)
function getUserData(userId, callback) {
    fetchUser(userId, (error, user) => {
        if (error) {
            callback(error);
            return;
        }
        
        fetchUserPosts(user.id, (error, posts) => {
            if (error) {
                callback(error);
                return;
            }
            
            fetchPostComments(posts[0].id, (error, comments) => {
                if (error) {
                    callback(error);
                    return;
                }
                
                callback(null, { user, posts, comments });
            });
        });
    });
}

// Using the nested callbacks
getUserData(1, (error, data) => {
    if (error) {
        console.error('Error:', error);
        return;
    }
    console.log('User data:', data);
});

Promises

Promises provide a cleaner way to handle asynchronous operations and avoid callback hell.

// Creating a promise
const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve({ id: 1, name: 'John' });
            } else {
                reject(new Error('Failed to fetch data'));
            }
        }, 1000);
    });
};

// Using the promise
fetchData()
    .then(data => {
        console.log('Data:', data);
        return data.id;
    })
    .then(id => {
        console.log('ID:', id);
        return fetchData(); // Return another promise
    })
    .then(data => {
        console.log('Data 2:', data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    })
    .finally(() => {
        console.log('Promise completed');
    });

// Refactoring the callback hell example with promises
function fetchUser(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ id: userId, name: 'John' });
        }, 1000);
    });
}

function fetchUserPosts(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve([
                { id: 1, title: 'Post 1' },
                { id: 2, title: 'Post 2' }
            ]);
        }, 1000);
    });
}

function fetchPostComments(postId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve([
                { id: 1, text: 'Comment 1' },
                { id: 2, text: 'Comment 2' }
            ]);
        }, 1000);
    });
}

// Using promise chaining
function getUserData(userId) {
    let userData = {};
    
    return fetchUser(userId)
        .then(user => {
            userData.user = user;
            return fetchUserPosts(user.id);
        })
        .then(posts => {
            userData.posts = posts;
            return fetchPostComments(posts[0].id);
        })
        .then(comments => {
            userData.comments = comments;
            return userData;
        });
}

getUserData(1)
    .then(data => {
        console.log('User data:', data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

Promise Methods

Promises provide several static methods for handling multiple asynchronous operations.

// Promise.all - wait for all promises to resolve
Promise.all([
    fetchUser(1),
    fetchUserPosts(1),
    fetchPostComments(1)
])
    .then(([user, posts, comments]) => {
        console.log('User:', user);
        console.log('Posts:', posts);
        console.log('Comments:', comments);
    })
    .catch(error => {
        // If any promise rejects, the catch block is executed
        console.error('Error:', error.message);
    });

// Promise.race - wait for the first promise to resolve or reject
Promise.race([
    fetchUser(1),
    new Promise((resolve) => setTimeout(() => resolve('Timeout'), 500))
])
    .then(result => {
        console.log('First result:', result);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

// Promise.allSettled - wait for all promises to settle (resolve or reject)
Promise.allSettled([
    fetchUser(1),
    Promise.reject(new Error('Failed to fetch posts')),
    fetchPostComments(1)
])
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`Promise ${index + 1} fulfilled:`, result.value);
            } else {
                console.log(`Promise ${index + 1} rejected:`, result.reason);
            }
        });
    });

// Promise.any - wait for the first promise to resolve
Promise.any([
    new Promise((resolve) => setTimeout(() => resolve('Result 1'), 1000)),
    new Promise((resolve) => setTimeout(() => resolve('Result 2'), 500)),
    new Promise((resolve) => setTimeout(() => resolve('Result 3'), 1500))
])
    .then(result => {
        console.log('First resolved result:', result); // Result 2
    })
    .catch(error => {
        console.error('All promises rejected:', error);
    });

Async/Await

Async/await is syntactic sugar built on top of promises, making asynchronous code look and behave more like synchronous code.

// Basic async/await
async function fetchData() {
    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error:', error.message);
        throw error;
    }
}

// Using async/await with our previous example
async function getUserData(userId) {
    try {
        const user = await fetchUser(userId);
        const posts = await fetchUserPosts(user.id);
        const comments = await fetchPostComments(posts[0].id);
        
        return { user, posts, comments };
    } catch (error) {
        console.error('Error:', error.message);
        throw error;
    }
}

// Calling the async function
async function main() {
    try {
        const userData = await getUserData(1);
        console.log('User data:', userData);
    } catch (error) {
        console.error('Main error:', error.message);
    }
}

main();

// Parallel execution with async/await
async function getUserDataParallel(userId) {
    try {
        const user = await fetchUser(userId);
        
        // Fetch posts and comments in parallel
        const [posts, profile] = await Promise.all([
            fetchUserPosts(user.id),
            fetchUserProfile(user.id)
        ]);
        
        const comments = await fetchPostComments(posts[0].id);
        
        return { user, posts, profile, comments };
    } catch (error) {
        console.error('Error:', error.message);
        throw error;
    }
}

// Async IIFE (Immediately Invoked Function Expression)
(async () => {
    try {
        const userData = await getUserDataParallel(1);
        console.log('User data (parallel):', userData);
    } catch (error) {
        console.error('Error:', error.message);
    }
})();

Error Handling in Asynchronous Code

Proper error handling is crucial in asynchronous code to prevent unhandled rejections and ensure robust applications.

// Error handling with callbacks
function fetchWithCallback(url, callback) {
    setTimeout(() => {
        if (url.includes('error')) {
            callback(new Error('Network error'));
            return;
        }
        callback(null, { data: 'Success' });
    }, 1000);
}

fetchWithCallback('https://api.example.com/data', (error, data) => {
    if (error) {
        console.error('Callback error:', error.message);
        return;
    }
    console.log('Callback data:', data);
});

// Error handling with promises
function fetchWithPromise(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (url.includes('error')) {
                reject(new Error('Network error'));
                return;
            }
            resolve({ data: 'Success' });
        }, 1000);
    });
}

fetchWithPromise('https://api.example.com/data')
    .then(data => {
        console.log('Promise data:', data);
    })
    .catch(error => {
        console.error('Promise error:', error.message);
    });

// Error handling with async/await
async function fetchWithAsync(url) {
    try {
        const data = await fetchWithPromise(url);
        console.log('Async data:', data);
    } catch (error) {
        console.error('Async error:', error.message);
        // Optionally rethrow or return a default value
        return { data: 'Default data' };
    }
}

// Global unhandled rejection handler
window.addEventListener('unhandledrejection', event => {
    console.error('Unhandled rejection:', event.reason);
    event.preventDefault(); // Prevent the default handling
});

Generators and Async Iterators

Generators and async iterators provide powerful ways to work with asynchronous code and data streams.

// Basic generator
function* simpleGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const generator = simpleGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

// Generator with input
function* twoWayGenerator() {
    const a = yield 'First yield';
    console.log('Received:', a);
    
    const b = yield 'Second yield';
    console.log('Received:', b);
    
    return 'Generator done';
}

const twoWay = twoWayGenerator();
console.log(twoWay.next()); // { value: 'First yield', done: false }
console.log(twoWay.next('Value A')); // Logs 'Received: Value A', returns { value: 'Second yield', done: false }
console.log(twoWay.next('Value B')); // Logs 'Received: Value B', returns { value: 'Generator done', done: true }

// Async generators
async function* asyncGenerator() {
    yield await Promise.resolve(1);
    yield await Promise.resolve(2);
    yield await Promise.resolve(3);
}

async function consumeAsyncGenerator() {
    const generator = asyncGenerator();
    
    for await (const value of generator) {
        console.log('Async value:', value);
    }
}

consumeAsyncGenerator();

// Using generators for asynchronous control flow
function fetchWithTimeout(url, timeout) {
    return new Promise((resolve, reject) => {
        const controller = new AbortController();
        const { signal } = controller;
        
        const timeoutId = setTimeout(() => {
            controller.abort();
            reject(new Error('Request timed out'));
        }, timeout);
        
        fetch(url, { signal })
            .then(response => {
                clearTimeout(timeoutId);
                return response.json();
            })
            .then(resolve)
            .catch(reject);
    });
}

function* fetchSequence(urls) {
    try {
        for (const url of urls) {
            const data = yield fetchWithTimeout(url, 5000);
            console.log('Data:', data);
        }
        return 'All fetches completed';
    } catch (error) {
        console.error('Fetch error:', error.message);
        return 'Fetch sequence failed';
    }
}

function runGenerator(generator, value) {
    const next = generator.next(value);
    
    if (next.done) {
        return next.value;
    }
    
    return Promise.resolve(next.value)
        .then(result => runGenerator(generator, result))
        .catch(error => {
            return generator.throw(error);
        });
}

Web Workers

Web Workers allow you to run JavaScript in background threads, separate from the main execution thread.

// main.js
function startWorker() {
    // Create a new worker
    const worker = new Worker('worker.js');
    
    // Send message to worker
    worker.postMessage({
        command: 'calculate',
        data: { numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
    });
    
    // Receive message from worker
    worker.onmessage = (event) => {
        console.log('Result from worker:', event.data);
        
        // Terminate worker when done
        worker.terminate();
    };
    
    // Handle errors
    worker.onerror = (error) => {
        console.error('Worker error:', error.message);
    };
}

// worker.js
self.onmessage = (event) => {
    const { command, data } = event.data;
    
    if (command === 'calculate') {
        // Perform CPU-intensive calculation
        const result = calculateSum(data.numbers);
        
        // Send result back to main thread
        self.postMessage({ result });
    }
};

function calculateSum(numbers) {
    // Simulate a complex calculation
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i % 10;
    }
    
    // Calculate the actual sum
    const actualSum = numbers.reduce((total, num) => total + num, 0);
    
    return actualSum;
}

Modules and Bundlers

JavaScript modules allow you to organize code into reusable, encapsulated pieces. Bundlers help package these modules for production.

Module Systems

JavaScript has several module systems, each with its own syntax and features.

// CommonJS (Node.js)
// math.js
const PI = 3.14159;

function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

module.exports = {
    PI,
    add,
    subtract
};

// main.js
const math = require('./math');
console.log(math.add(2, 3)); // 5

// Destructuring in CommonJS
const { add, PI } = require('./math');
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

// AMD (Asynchronous Module Definition)
// math.js
define([], function() {
    const PI = 3.14159;
    
    function add(a, b) {
        return a + b;
    }
    
    return {
        PI,
        add
    };
});

// main.js
require(['math'], function(math) {
    console.log(math.add(2, 3)); // 5
});

// UMD (Universal Module Definition)
// Works in both Node.js and browsers
(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define([], factory);
    } else if (typeof module === 'object' && module.exports) {
        // CommonJS
        module.exports = factory();
    } else {
        // Browser globals
        root.math = factory();
    }
}(typeof self !== 'undefined' ? self : this, function() {
    const PI = 3.14159;
    
    function add(a, b) {
        return a + b;
    }
    
    return {
        PI,
        add
    };
}));

// ES Modules (ESM)
// math.js
export const PI = 3.14159;

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export default function multiply(a, b) {
    return a * b;
}

// main.js
import multiply, { add, subtract, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6

ES Modules in Depth

ES Modules (ESM) are the official standard for JavaScript modules, with several powerful features.

// Named exports
// utils.js
export function formatDate(date) {
    return date.toLocaleDateString();
}

export function formatCurrency(amount) {
    return `$${amount.toFixed(2)}`;
}

export const API_URL = 'https://api.example.com';

// Default export
// user.js
export default class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
    
    getInfo() {
        return `${this.name} (${this.email})`;
    }
}

// Importing
// main.js
import User from './user.js';
import { formatDate, formatCurrency, API_URL as BASE_URL } from './utils.js';

const user = new User('John', '[email protected]');
console.log(user.getInfo()); // John ([email protected])
console.log(formatDate(new Date())); // e.g., "3/19/2025"
console.log(formatCurrency(99.99)); // $99.99
console.log(BASE_URL); // https://api.example.com

// Import all exports as a namespace
import * as utils from './utils.js';
console.log(utils.formatDate(new Date()));
console.log(utils.API_URL);

// Re-exporting
// index.js
export { default as User } from './user.js';
export { formatDate, formatCurrency } from './utils.js';
export { default as API } from './api.js';

// Dynamic imports
async function loadModule() {
    try {
        const module = await import('./heavy-module.js');
        module.initialize();
    } catch (error) {
        console.error('Failed to load module:', error);
    }
}

// Load module on button click
document.getElementById('load-button').addEventListener('click', loadModule);

Module Bundlers

Module bundlers like Webpack, Rollup, and Parcel combine your modular code into optimized bundles for production.

// Webpack configuration example
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.[contenthash].js',
        clean: true
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            },
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            },
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html'
        }),
        new MiniCssExtractPlugin({
            filename: 'styles.[contenthash].css'
        })
    ],
    devServer: {
        static: {
            directory: path.join(__dirname, 'dist')
        },
        port: 3000,
        open: true,
        hot: true
    },
    mode: 'development',
    devtool: 'source-map'
};

// Rollup configuration example
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
import css from 'rollup-plugin-css-only';

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife',
        sourcemap: true
    },
    plugins: [
        resolve(),
        commonjs(),
        babel({
            babelHelpers: 'bundled',
            exclude: 'node_modules/**'
        }),
        css({ output: 'bundle.css' }),
        terser()
    ]
};

Package Managers

Package managers like npm and Yarn help you manage dependencies and run scripts.

// package.json example
{
    "name": "my-app",
    "version": "1.0.0",
    "description": "My awesome app",
    "main": "dist/index.js",
    "scripts": {
        "start": "webpack serve",
        "build": "webpack --mode production",
        "test": "jest",
        "lint": "eslint src/**/*.js"
    },
    "dependencies": {
        "axios": "^0.24.0",
        "lodash": "^4.17.21",
        "react": "^17.0.2",
        "react-dom": "^17.0.2"
    },
    "devDependencies": {
        "@babel/core": "^7.16.0",
        "@babel/preset-env": "^7.16.0",
        "@babel/preset-react": "^7.16.0",
        "babel-loader": "^8.2.3",
        "css-loader": "^6.5.1",
        "eslint": "^8.2.0",
        "html-webpack-plugin": "^5.5.0",
        "jest": "^27.3.1",
        "mini-css-extract-plugin": "^2.4.4",
        "webpack": "^5.64.0",
        "webpack-cli": "^4.9.1",
        "webpack-dev-server": "^4.5.0"
    },
    "engines": {
        "node": ">=14.0.0"
    },
    "license": "MIT"
}

// npm commands
// npm install - Install all dependencies
// npm install axios - Install a specific package
// npm install eslint --save-dev - Install a dev dependency
// npm run build - Run the build script
// npm run start - Run the start script

// Yarn commands
// yarn - Install all dependencies
// yarn add axios - Install a specific package
// yarn add eslint --dev - Install a dev dependency
// yarn build - Run the build script
// yarn start - Run the start script

Tree Shaking

Tree shaking is a technique used by bundlers to eliminate unused code from the final bundle.

// utils.js
export function formatDate(date) {
    return date.toLocaleDateString();
}

export function formatTime(date) {
    return date.toLocaleTimeString();
}

export function formatCurrency(amount) {
    return `$${amount.toFixed(2)}`;
}

// main.js
import { formatDate, formatCurrency } from './utils.js';

console.log(formatDate(new Date()));
console.log(formatCurrency(99.99));

// In the final bundle, formatTime will be excluded because it's not used

Code Splitting

Code splitting allows you to split your code into smaller chunks that can be loaded on demand.

// Without code splitting
import { Chart } from 'chart.js';
import { formatData } from './utils.js';

function renderChart() {
    const data = formatData([1, 2, 3, 4, 5]);
    const chart = new Chart('chart', {
        type: 'bar',
        data
    });
}

// With code splitting
function renderChart() {
    import(/* webpackChunkName: "chart" */ 'chart.js')
        .then(({ Chart }) => {
            import(/* webpackChunkName: "utils" */ './utils.js')
                .then(({ formatData }) => {
                    const data = formatData([1, 2, 3, 4, 5]);
                    const chart = new Chart('chart', {
                        type: 'bar',
                        data
                    });
                });
        })
        .catch(error => {
            console.error('Failed to load chart:', error);
        });
}

// React code splitting with lazy and Suspense
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
    return (
        
            Loading...
}> ); }

Design Patterns

Design patterns are reusable solutions to common problems in software design. They provide templates for solving specific types of problems.

Creational Patterns

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

// Singleton Pattern
class Singleton {
    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        
        Singleton.instance = this;
        this.data = {};
    }
    
    set(key, value) {
        this.data[key] = value;
    }
    
    get(key) {
        return this.data[key];
    }
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true

// Factory Pattern
class User {
    constructor(name, role) {
        this.name = name;
        this.role = role;
    }
}

class UserFactory {
    createUser(name, role) {
        switch (role) {
            case 'admin':
                return new Admin(name);
            case 'editor':
                return new Editor(name);
            default:
                return new User(name, 'user');
        }
    }
}

class Admin extends User {
    constructor(name) {
        super(name, 'admin');
        this.permissions = ['read', 'write', 'delete', 'manage'];
    }
}

class Editor extends User {
    constructor(name) {
        super(name, 'editor');
        this.permissions = ['read', 'write'];
    }
}

const factory = new UserFactory();
const admin = factory.createUser('John', 'admin');
const editor = factory.createUser('Jane', 'editor');
const user = factory.createUser('Bob', 'user');

// Builder Pattern
class Car {
    constructor() {
        this.make = '';
        this.model = '';
        this.year = 0;
        this.color = '';
        this.features = [];
    }
}

class CarBuilder {
    constructor() {
        this.car = new Car();
    }
    
    setMake(make) {
        this.car.make = make;
        return this;
    }
    
    setModel(model) {
        this.car.model = model;
        return this;
    }
    
    setYear(year) {
        this.car.year = year;
        return this;
    }
    
    setColor(color) {
        this.car.color = color;
        return this;
    }
    
    addFeature(feature) {
        this.car.features.push(feature);
        return this;
    }
    
    build() {
        return this.car;
    }
}

const car = new CarBuilder()
    .setMake('Toyota')
    .setModel('Camry')
    .setYear(2022)
    .setColor('Blue')
    .addFeature('Bluetooth')
    .addFeature('Backup Camera')
    .build();

Structural Patterns

Structural patterns deal with object composition, creating relationships between objects to form larger structures.

// Adapter Pattern
class OldAPI {
    getUsers() {
        return [
            { name: 'John', age: 30 },
            { name: 'Jane', age: 25 }
        ];
    }
}

class NewAPI {
    fetchUsers() {
        return [
            { firstName: 'John', lastName: 'Doe', age: 30 },
            { firstName: 'Jane', lastName: 'Doe', age: 25 }
        ];
    }
}

class APIAdapter {
    constructor(newAPI) {
        this.newAPI = newAPI;
    }
    
    getUsers() {
        const users = this.newAPI.fetchUsers();
        return users.map(user => ({
            name: `${user.firstName} ${user.lastName}`,
            age: user.age
        }));
    }
}

function displayUsers(api) {
    const users = api.getUsers();
    users.forEach(user => {
        console.log(`${user.name}, ${user.age}`);
    });
}

const oldAPI = new OldAPI();
displayUsers(oldAPI);

const newAPI = new NewAPI();
const adapter = new APIAdapter(newAPI);
displayUsers(adapter);

// Decorator Pattern
class Coffee {
    cost() {
        return 5;
    }
    
    description() {
        return 'Coffee';
    }
}

class MilkDecorator {
    constructor(coffee) {
        this.coffee = coffee;
    }
    
    cost() {
        return this.coffee.cost() + 1;
    }
    
    description() {
        return `${this.coffee.description()} with milk`;
    }
}

class SugarDecorator {
    constructor(coffee) {
        this.coffee = coffee;
    }
    
    cost() {
        return this.coffee.cost() + 0.5;
    }
    
    description() {
        return `${this.coffee.description()} with sugar`;
    }
}

let coffee = new Coffee();
console.log(coffee.description()); // Coffee
console.log(coffee.cost()); // 5

coffee = new MilkDecorator(coffee);
console.log(coffee.description()); // Coffee with milk
console.log(coffee.cost()); // 6

coffee = new SugarDecorator(coffee);
console.log(coffee.description()); // Coffee with milk with sugar
console.log(coffee.cost()); // 6.5

// Proxy Pattern
class RealImage {
    constructor(filename) {
        this.filename = filename;
        this.loadFromDisk();
    }
    
    loadFromDisk() {
        console.log(`Loading ${this.filename} from disk`);
    }
    
    display() {
        console.log(`Displaying ${this.filename}`);
    }
}

class ProxyImage {
    constructor(filename) {
        this.filename = filename;
        this.realImage = null;
    }
    
    display() {
        if (!this.realImage) {
            this.realImage = new RealImage(this.filename);
        }
        this.realImage.display();
    }
}

const image1 = new ProxyImage('image1.jpg');
const image2 = new ProxyImage('image2.jpg');

// Image1 is loaded and displayed
image1.display();

// Image1 is only displayed (already loaded)
image1.display();

// Image2 is loaded and displayed
image2.display();

Behavioral Patterns

Behavioral patterns deal with communication between objects, how objects interact and distribute responsibility.

// Observer Pattern
class Subject {
    constructor() {
        this.observers = [];
    }
    
    subscribe(observer) {
        this.observers.push(observer);
    }
    
    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }
    
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

class Observer {
    constructor(name) {
        this.name = name;
    }
    
    update(data) {
        console.log(`${this.name} received: ${data}`);
    }
}

const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('Hello observers!');

subject.unsubscribe(observer1);

subject.notify('Hello again!');

// Strategy Pattern
class ShippingStrategy {
    calculate(package) {
        // Abstract method
    }
}

class FedExStrategy extends ShippingStrategy {
    calculate(package) {
        return package.weight * 4.5;
    }
}

class UPSStrategy extends ShippingStrategy {
    calculate(package) {
        return package.weight * 3.25 + 10;
    }
}

class USPSStrategy extends ShippingStrategy {
    calculate(package) {
        return package.weight * 2.5 + 5;
    }
}

class Shipping {
    constructor() {
        this.strategy = null;
    }
    
    setStrategy(strategy) {
        this.strategy = strategy;
    }
    
    calculate(package) {
        if (!this.strategy) {
            throw new Error('No shipping strategy set');
        }
        return this.strategy.calculate(package);
    }
}

const package = { weight: 10 };
const shipping = new Shipping();

shipping.setStrategy(new FedExStrategy());
console.log(`FedEx: $${shipping.calculate(package)}`);

shipping.setStrategy(new UPSStrategy());
console.log(`UPS: $${shipping.calculate(package)}`);

shipping.setStrategy(new USPSStrategy());
console.log(`USPS: $${shipping.calculate(package)}`);

// Command Pattern
class Light {
    turnOn() {
        console.log('Light is on');
    }
    
    turnOff() {
        console.log('Light is off');
    }
}

class LightOnCommand {
    constructor(light) {
        this.light = light;
    }
    
    execute() {
        this.light.turnOn();
    }
}

class LightOffCommand {
    constructor(light) {
        this.light = light;
    }
    
    execute() {
        this.light.turnOff();
    }
}

class RemoteControl {
    constructor() {
        this.commands = {};
    }
    
    setCommand(button, command) {
        this.commands[button] = command;
    }
    
    pressButton(button) {
        if (this.commands[button]) {
            this.commands[button].execute();
        } else {
            console.log(`Button ${button} not programmed`);
        }
    }
}

const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);

const remote = new RemoteControl();
remote.setCommand('on', lightOn);
remote.setCommand('off', lightOff);

remote.pressButton('on'); // Light is on
remote.pressButton('off'); // Light is off
remote.pressButton('dim'); // Button dim not programmed

Module Pattern

The Module Pattern is one of the most common design patterns in JavaScript, used to create private and public methods and variables.

// Basic Module Pattern
const calculator = (function() {
    // Private variables and functions
    let result = 0;
    
    function add(a, b) {
        return a + b;
    }
    
    function subtract(a, b) {
        return a - b;
    }
    
    // Public API
    return {
        add: function(a, b) {
            result = add(a, b);
            return result;
        },
        subtract: function(a, b) {
            result = subtract(a, b);
            return result;
        },
        getResult: function() {
            return result;
        }
    };
})();

console.log(calculator.add(5, 3)); // 8
console.log(calculator.subtract(10, 4)); // 6
console.log(calculator.getResult()); // 6

// Revealing Module Pattern
const counter = (function() {
    let count = 0;
    
    function increment() {
        count++;
    }
    
    function decrement() {
        count--;
    }
    
    function getCount() {
        return count;
    }
    
    function reset() {
        count = 0;
    }
    
    // Reveal only the public methods
    return {
        increment,
        decrement,
        getCount,
        reset
    };
})();

counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
counter.decrement();
console.log(counter.getCount()); // 1
counter.reset();
console.log(counter.getCount()); // 0

MVC, MVP, and MVVM Patterns

These architectural patterns help organize code in applications with user interfaces.

// MVC (Model-View-Controller) Pattern
// Model
class UserModel {
    constructor() {
        this.users = [];
    }
    
    addUser(user) {
        this.users.push(user);
    }
    
    getUsers() {
        return this.users;
    }
    
    deleteUser(index) {
        this.users.splice(index, 1);
    }
}

// View
class UserView {
    constructor() {
        this.app = document.getElementById('app');
    }
    
    render(users) {
        this.app.innerHTML = '';
        
        const ul = document.createElement('ul');
        
        users.forEach((user, index) => {
            const li = document.createElement('li');
            li.textContent = `${user.name} (${user.email})`;
            
            const deleteButton = document.createElement('button');
            deleteButton.textContent = 'Delete';
            deleteButton.dataset.index = index;
            deleteButton.className = 'delete-btn';
            
            li.appendChild(deleteButton);
            ul.appendChild(li);
        });
        
        this.app.appendChild(ul);
        
        const form = document.createElement('form');
        form.innerHTML = `
            
            
            
        `;
        
        this.app.appendChild(form);
    }
    
    bindAddUser(handler) {
        const form = this.app.querySelector('form');
        form.addEventListener('submit', event => {
            event.preventDefault();
            
            const name = document.getElementById('name').value;
            const email = document.getElementById('email').value;
            
            handler({ name, email });
            
            form.reset();
        });
    }
    
    bindDeleteUser(handler) {
        this.app.addEventListener('click', event => {
            if (event.target.className === 'delete-btn') {
                const index = parseInt(event.target.dataset.index);
                handler(index);
            }
        });
    }
}

// Controller
class UserController {
    constructor(model, view) {
        this.model = model;
        this.view = view;
        
        // Bind event handlers
        this.view.bindAddUser(this.handleAddUser.bind(this));
        this.view.bindDeleteUser(this.handleDeleteUser.bind(this));
        
        // Initial render
        this.updateView();
    }
    
    updateView() {
        this.view.render(this.model.getUsers());
    }
    
    handleAddUser(user) {
        this.model.addUser(user);
        this.updateView();
    }
    
    handleDeleteUser(index) {
        this.model.deleteUser(index);
        this.updateView();
    }
}

// Usage
const app = new UserController(new UserModel(), new UserView());

Testing and Debugging

Testing and debugging are essential skills for writing reliable JavaScript code.

Unit Testing

Unit testing involves testing individual units of code in isolation.

// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export function multiply(a, b) {
    return a * b;
}

export function divide(a, b) {
    if (b === 0) {
        throw new Error('Division by zero');
    }
    return a / b;
}

// math.test.js (Jest)
import { add, subtract, multiply, divide } from './math';

describe('Math functions', () => {
    test('add should correctly add two numbers', () => {
        expect(add(2, 3)).toBe(5);
        expect(add(-1, 1)).toBe(0);
        expect(add(0, 0)).toBe(0);
    });
    
    test('subtract should correctly subtract two numbers', () => {
        expect(subtract(5, 3)).toBe(2);
        expect(subtract(1, 1)).toBe(0);
        expect(subtract(0, 5)).toBe(-5);
    });
    
    test('multiply should correctly multiply two numbers', () => {
        expect(multiply(2, 3)).toBe(6);
        expect(multiply(-2, 3)).toBe(-6);
        expect(multiply(0, 5)).toBe(0);
    });
    
    test('divide should correctly divide two numbers', () => {
        expect(divide(6, 3)).toBe(2);
        expect(divide(5, 2)).toBe(2.5);
        expect(divide(0, 5)).toBe(0);
    });
    
    test('divide should throw an error when dividing by zero', () => {
        expect(() => divide(5, 0)).toThrow('Division by zero');
    });
});

Integration Testing

Integration testing involves testing how different parts of the application work together.

// api.js
export class API {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    
    async fetchUsers() {
        const response = await fetch(`${this.baseURL}/users`);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    }
    
    async createUser(user) {
        const response = await fetch(`${this.baseURL}/users`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(user)
        });
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    }
}

// userService.js
import { API } from './api';

export class UserService {
    constructor(baseURL) {
        this.api = new API(baseURL);
    }
    
    async getUsers() {
        return this.api.fetchUsers();
    }
    
    async addUser(name, email) {
        if (!name || !email) {
            throw new Error('Name and email are required');
        }
        
        return this.api.createUser({ name, email });
    }
}

Debugging Techniques

Effective debugging is essential for finding and fixing issues in your code.

// Console methods
console.log('Basic logging');
console.info('Informational message');
console.warn('Warning message');
console.error('Error message');

// Logging objects
const user = { name: 'John', age: 30, email: '[email protected]' };
console.log('User:', user);
console.table(user); // Displays object as a table
console.dir(user); // Displays object with expandable properties

// Grouping logs
console.group('User Details');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.log('Email:', user.email);
console.groupEnd();

// Timing operations
console.time('Operation');
// ... some operation
console.timeEnd('Operation'); // Operation: 123.45ms

// Counting occurrences
for (let i = 0; i < 5; i++) {
    console.count('Loop iteration');
}

// Conditional logging
console.assert(user.age > 18, 'User is not an adult');

// Stack traces
console.trace('Trace message');

// Debugger statement
function buggyFunction() {
    let x = 10;
    let y = 0;
    debugger; // Execution will pause here when DevTools is open
    return x / y;
}

// Try-catch for debugging
try {
    const result = buggyFunction();
    console.log('Result:', result);
} catch (error) {
    console.error('Error caught:', error.message);
    console.error('Stack trace:', error.stack);
}

Practice Exercises

Now that you've learned advanced JavaScript concepts, it's time to practice! Here are some exercises to help you reinforce what you've learned.

Exercise 1: Closures and Scope

Create a counter factory function that generates counter objects with the following methods:

  • increment(): Increases the counter by 1
  • decrement(): Decreases the counter by 1
  • getValue(): Returns the current counter value
  • reset(): Resets the counter to its initial value

The factory function should take an initial value as a parameter.

Exercise 2: Prototypes and Inheritance

Create a class hierarchy for a library system:

  • LibraryItem: Base class with properties like title, year, and isCheckedOut
  • Book: Extends LibraryItem with additional properties like author and pages
  • DVD: Extends LibraryItem with additional properties like director and duration
  • Magazine: Extends LibraryItem with additional properties like issue and publisher

Implement methods for checking items out, returning items, and displaying item information.

Exercise 3: Asynchronous Programming

Create a function that simulates fetching user data from an API, then fetching their posts, and finally fetching comments for the first post. Implement this function using:

  • Callbacks
  • Promises
  • Async/await

Compare the three implementations and discuss the advantages and disadvantages of each approach.

Exercise 4: Design Patterns

Implement a shopping cart system using the following design patterns:

  • Singleton: Ensure there's only one cart instance
  • Factory: Create different types of products (e.g., physical, digital, subscription)
  • Observer: Notify observers when the cart changes (e.g., for updating the UI)
  • Strategy: Implement different discount strategies (e.g., percentage discount, fixed amount discount, buy-one-get-one-free)

Exercise 5: Testing

Write unit tests for a function that validates a user object with the following requirements:

  • Name must be a string with at least 2 characters
  • Email must be a valid email format
  • Age must be a number between 18 and 120
  • Password must be at least 8 characters and contain at least one uppercase letter, one lowercase letter, and one number

Use a testing framework like Jest and write tests for both valid and invalid inputs.

Next Steps

Now that you've mastered advanced JavaScript concepts, you can continue your learning journey with:

  • Exploring popular JavaScript frameworks like React, Vue, or Angular
  • Learning about server-side JavaScript with Node.js
  • Diving into TypeScript for static typing
  • Studying functional programming libraries like Ramda or Lodash/fp
  • Contributing to open-source JavaScript projects