Advanced JavaScript: 10 Challenging Coding Exercises

As you continue your journey in mastering JavaScript, tackling more complex and challenging exercises can significantly enhance your coding skills. Here are 10 advanced JavaScript exercises designed to push your understanding and help you become a more proficient developer. Each exercise includes complete code and explanations.

Exercise 1: Implementing a Debounce Function

Objective: Create a debounce function to limit the rate at which a function can fire.

Code:

function debounce(func, delay) {
let debounceTimer;
return function(...args) {
const context = this;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
}
}

// Example usage
const logMessage = debounce(() => console.log('Debounced function executed'), 2000);
logMessage();
logMessage();
logMessage(); // Only the last call will execute after 2 seconds

Explanation: The debounce function prevents a function from being called repeatedly within a short period. It uses setTimeout to delay the execution and clearTimeout to reset the timer if the function is called again within the delay period.

Exercise 2: Implementing a Throttle Function

Objective: Create a throttle function to limit the rate at which a function can fire.

Code:

function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
}
}

// Example usage
const logMessage = throttle(() => console.log('Throttled function executed'), 2000);
logMessage();
logMessage();
logMessage(); // Only the first call will execute immediately, the rest will execute after the specified limit

Explanation: The throttle function limits the execution of a function to once every specified period. It uses setTimeout to schedule the next execution if the function is called again before the limit period has passed.

Exercise 3: Deep Cloning an Object

Objective: Implement a function to deeply clone a JavaScript object.

Code:

function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
let copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepClone(obj[i]);
}
return copy;
}

if (obj instanceof Object) {
let copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
}

// Example usage
const original = { a: 1, b: { c: 2, d: [3, 4] } };
const cloned = deepClone(original);
console.log(cloned); // Output: { a: 1, b: { c: 2, d: [3, 4] } }

Explanation: The deepClone function recursively clones all properties of an object, ensuring that nested objects and arrays are also cloned.

Exercise 4: Implementing a Simple Pub/Sub System

Objective: Create a simple publish-subscribe system.

Code:

class PubSub {
constructor() {
this.events = {};
}

subscribe(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}

unsubscribe(event, listenerToRemove) {
if (!this.events[event]) return;

this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove);
}

publish(event, data) {
if (!this.events[event]) return;

this.events[event].forEach(listener => listener(data));
}
}

// Example usage
const pubsub = new PubSub();
const onMessage = data => console.log('Message received:', data);

pubsub.subscribe('message', onMessage);
pubsub.publish('message', { text: 'Hello, world!' }); // Output: Message received: { text: 'Hello, world!' }
pubsub.unsubscribe('message', onMessage);
pubsub.publish('message', { text: 'Hello again!' }); // No output

Explanation: The PubSub class manages event listeners for different events, allowing for the publishing of data to all subscribed listeners.

Exercise 5: Implementing a Simple Promise-based Retry Function

Objective: Create a function that retries a promise-returning function a specified number of times.

Code:

function retry(fn, retries = 3, delay = 1000) {
return new Promise((resolve, reject) => {
const attempt = () => {
fn()
.then(resolve)
.catch((error) => {
if (retries === 0) {
reject(error);
} else {
retries--;
setTimeout(attempt, delay);
}
});
};
attempt();
});
}

// Example usage
const fetchData = () => fetch('https://api.example.com/data').then(response => response.json());

retry(fetchData, 3, 2000)
.then(data => console.log('Data received:', data))
.catch(error => console.error('Failed after 3 retries:', error));

Explanation: The retry function retries a promise-returning function a specified number of times, with a delay between each attempt.

Exercise 6: Creating a Simple LRU Cache

Objective: Implement a simple Least Recently Used (LRU) cache.

Code:

class LRUCache {
constructor(limit) {
this.limit = limit;
this.cache = new Map();
}

get(key) {
if (!this.cache.has(key)) {
return null;
}
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}

set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.limit) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}

// Example usage
const cache = new LRUCache(2);
cache.set('a', 1);
cache.set('b', 2);
console.log(cache.get('a')); // Output: 1
cache.set('c', 3);
console.log(cache.get('b')); // Output: null (removed because of the limit)
console.log(cache.get('c')); // Output: 3

Explanation: The LRUCache class maintains a cache with a specified limit, removing the least recently used items when the limit is exceeded.

Exercise 7: Implementing a Custom Event Emitter

Objective: Create a custom event emitter class.

Code:

class EventEmitter {
constructor() {
this.events = {};
}

on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}

off(event, listenerToRemove) {
if (!this.events[event]) return;

this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove);
}

emit(event, data) {
if (!this.events[event]) return;

this.events[event].forEach(listener => listener(data));
}
}

// Example usage
const emitter = new EventEmitter();
const onEvent = data => console.log('Event received:', data);

emitter.on('event', onEvent);
emitter.emit('event', { message: 'Hello, world!' }); // Output: Event received: { message: 'Hello, world!' }
emitter.off('event', onEvent);
emitter.emit('event', { message: 'Hello again!' }); // No output

Explanation: The EventEmitter class allows for registering, deregistering, and emitting events with data.

Exercise 8: Implementing a Simple Router

Objective: Create a simple client-side router.

Code:

class Router {
constructor() {
this.routes = {};
window.addEventListener('hashchange', this.hashChangeHandler.bind(this));
window.addEventListener('load', this.hashChangeHandler.bind(this));
}

register(route, callback) {
this.routes[route] = callback;
}

hashChangeHandler() {
const hash = window.location.hash.substring(1);
if (this.routes[hash]) {
this.routes[hash]();
}
}
}

// Example usage
const router = new Router();
router.register('home', () => console.log('Home route'));
router.register('about', () => console.log('About route'));

// Navigate to #home or #about in the URL to see the respective messages.

Explanation: The Router class listens for hash changes and executes the corresponding callback function based on the registered routes.

Exercise 9: Creating a Simple Middleware System

Objective: Implement a middleware system similar to Express.js.

Code:

class Middleware {
constructor() {
this.middlewares = [];
}

use(fn) {
this.middlewares.push(fn);
}

execute(context) {
const dispatch = (i) => {
if (i >= this.middlewares.length) return;
const middleware = this.middlewares[i];
middleware(context, () => dispatch(i + 1));
};
dispatch(0);
}
}

// Example usage
const app = new Middleware();

app.use((ctx, next) => {
console.log('Middleware 1');
next();
});

app.use((ctx, next) => {
console.log('Middleware 2');
next();
});

app.execute({});
// Output:
// Middleware 1
// Middleware 2

Explanation: The Middleware class manages an array of middleware functions and executes them in sequence.

Exercise 10: Implementing a Custom Iterable

Objective: Create a custom iterable object.

Code:

class CustomIterable {
constructor(data) {
this.data = data;
}

[Symbol.iterator]() {
let index = 0;
let data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { done: true };
}
}
};
}
}

// Example usage
const iterable = new CustomIterable([1, 2, 3, 4]);
for (const value of iterable) {
console.log(value);
}
// Output:
// 1
// 2
// 3
// 4

Explanation: The CustomIterable class implements the iterator protocol by defining a [Symbol.iterator] method that returns an iterator object with a next method.

These advanced JavaScript exercises cover a range of topics, from implementing debounce and throttle functions to creating custom iterables and middleware systems. They provide a deeper understanding of JavaScript concepts and enhance coding skills. Happy coding!