20+ hands-on JavaScript coding challenges covering array methods, closures, higher-order functions, destructuring, spread/rest, object manipulation, memoization, currying, and DOM concepts.
This set contains 22 coding challenges to strengthen your intermediate JavaScript skills. Each problem has a statement, working solution, and expected output.
Problem 2 — Deep Clone an Object
Create a function that produces a true deep copy of an object, handling nested objects and arrays without reference sharing.
function deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(item => deepClone(item));
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = deepClone(obj[key]);
}
return cloned;
}
const original = { a: 1, b: { c: 2, d: [3, 4, { e: 5 }] } };
const copy = deepClone(original);
copy.b.c = 99;
copy.b.d[2].e = 100;
console.log(original.b.c);
console.log(original.b.d[2].e);
console.log(copy.b.c);
console.log(copy.b.d[2].e);
Key Concept: A recursive approach checks each value's type. Arrays clone via map, objects via key iteration. Mutations on the clone never affect the original.
Problem 3 — Debounce Function
Implement a debounce utility that delays function execution until a specified time has passed since the last invocation.
function debounce(fn, delay) {
let timeoutId = null;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
let callCount = 0;
const increment = debounce(() => { callCount++; }, 100);
increment();
increment();
increment();
setTimeout(() => {
console.log("Call count:", callCount);
}, 200);
Key Concept: The closure captures timeoutId. Each call clears the previous timer, so only the final call within the delay window executes. Essential for search inputs, resize, and scroll handlers.
Problem 4 — Group Array of Objects by Property
Write a function that groups an array of objects by a specified key, returning an object whose keys are the grouped values.
function groupBy(arr, key) {
return arr.reduce((groups, item) => {
const groupKey = item[key];
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(item);
return groups;
}, {});
}
const people = [
{ name: "Alice", department: "Engineering" },
{ name: "Bob", department: "Marketing" },
{ name: "Charlie", department: "Engineering" },
{ name: "Diana", department: "Marketing" },
{ name: "Eve", department: "Design" }
];
const grouped = groupBy(people, "department");
console.log(JSON.stringify(grouped, null, 2));
{
"Engineering": [
{ "name": "Alice", "department": "Engineering" },
{ "name": "Charlie", "department": "Engineering" }
],
"Marketing": [
{ "name": "Bob", "department": "Marketing" },
{ "name": "Diana", "department": "Marketing" }
],
"Design": [
{ "name": "Eve", "department": "Design" }
]
}Key Concept: reduce builds an accumulator object. Each item's property value becomes a key, and the item is pushed into the corresponding array.
Problem 5 — Remove Duplicates (Multiple Approaches)
Remove duplicate values from an array using three different techniques.
const nums = [1, 2, 2, 3, 4, 4, 5, 1, 3, 6];
// Method 1: Set
const unique1 = [...new Set(nums)];
// Method 2: filter + indexOf
const unique2 = nums.filter((val, idx) => nums.indexOf(val) === idx);
// Method 3: reduce
const unique3 = nums.reduce((acc, val) => {
if (!acc.includes(val)) acc.push(val);
return acc;
}, []);
console.log("Set:", unique1);
console.log("Filter:", unique2);
console.log("Reduce:", unique3);
Set: [1, 2, 3, 4, 5, 6]
Filter: [1, 2, 3, 4, 5, 6]
Reduce: [1, 2, 3, 4, 5, 6]
Key Concept: Set is O(n), filter + indexOf is O(n²) but readable, reduce offers explicit control. Choose based on performance needs.
Problem 6 — Implement Array.prototype.map
Recreate the built-in map method from scratch to understand its internal mechanics.
Array.prototype.myMap = function (callback, thisArg) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this) {
result[i] = callback.call(thisArg, this[i], i, this);
}
}
return result;
};
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.myMap(n => n * 2);
const indexed = numbers.myMap((val, idx) => `${idx}:${val}`);
console.log(doubled);
console.log(indexed);
[2, 4, 6, 8, 10]
["0:1", "1:2", "2:3", "3:4", "4:5"]
Key Concept: The real map passes three arguments (element, index, array), respects thisArg, skips holes (i in this), and returns a new array of the same length.
Problem 7 — Memoize Function
Create a memoization wrapper that caches results of expensive function calls based on their arguments.
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;
};
}
let computeCount = 0;
const factorial = memoize(function (n) {
computeCount++;
if (n <= 1) return 1;
return n * factorial(n - 1);
});
console.log(factorial(5));
console.log(factorial(5));
console.log(factorial(3));
console.log("Compute calls:", computeCount);
120
120
6
Compute calls: 6
Key Concept: Map stores stringified argument keys mapped to results. Repeated calls with identical arguments return cached values instantly. Critical for optimizing recursive computations.
Problem 8 — Curry Function
Implement a generic curry utility that transforms a multi-argument function into a chain of single-argument functions.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
const multiply = (a, b, c) => a * b * c;
const curriedMultiply = curry(multiply);
console.log(curriedMultiply(2)(3)(4));
console.log(curriedMultiply(2, 3)(4));
console.log(curriedMultiply(2)(3, 4));
console.log(curriedMultiply(2, 3, 4));
Key Concept: Currying checks if enough arguments are collected (args.length >= fn.length). If not, it returns a new function that accumulates more. This enables partial application and composition.
Problem 9 — Chunk Array
Split an array into smaller arrays of a specified size.
function chunk(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
console.log(JSON.stringify(chunk([1, 2, 3, 4, 5, 6, 7], 3)));
console.log(JSON.stringify(chunk([1, 2, 3, 4, 5, 6], 2)));
console.log(JSON.stringify(chunk([1, 2, 3], 5)));
[[1,2,3],[4,5,6],[7]]
[[1,2],[3,4],[5,6]]
[[1,2,3]]
Key Concept: slice(i, i + size) extracts sub-arrays without mutation. The loop increments by size. The last chunk may be smaller than size.
Problem 10 — Zip Arrays
Combine multiple arrays element-by-element into an array of tuples.
function zip(...arrays) {
const maxLen = Math.max(...arrays.map(a => a.length));
const result = [];
for (let i = 0; i < maxLen; i++) {
result.push(arrays.map(a => a[i]));
}
return result;
}
const names = ["Alice", "Bob", "Charlie"];
const ages = [25, 30, 35];
const cities = ["NYC", "LA", "Chicago"];
console.log(JSON.stringify(zip(names, ages, cities)));
console.log(JSON.stringify(zip([1, 2], [3, 4, 5])));
[["Alice",25,"NYC"],["Bob",30,"LA"],["Charlie",35,"Chicago"]]
[[1,3],[2,4],[undefined,5]]
Key Concept: The rest parameter collects all arguments. The function iterates to the longest array's length, pulling the i-th element from each. Missing elements become undefined.
Problem 11 — Implement Array.prototype.reduce
Build the reduce method from scratch to understand accumulator mechanics.
Array.prototype.myReduce = function (callback, initialValue) {
let accumulator;
let startIndex;
if (initialValue !== undefined) {
accumulator = initialValue;
startIndex = 0;
} else {
if (this.length === 0) {
throw new TypeError("Reduce of empty array with no initial value");
}
accumulator = this[0];
startIndex = 1;
}
for (let i = startIndex; i < this.length; i++) {
if (i in this) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};
const nums = [1, 2, 3, 4, 5];
console.log(nums.myReduce((sum, n) => sum + n, 0));
console.log(nums.myReduce((max, n) => (n > max ? n : max)));
console.log(nums.myReduce((acc, n) => acc * n, 1));
Key Concept: Without an initial value, element 0 becomes the accumulator and iteration starts at index 1. The callback receives: accumulator, current value, index, array.
Problem 12 — Object Destructuring and Defaults
Use destructuring with default values, renaming, and nested patterns to extract data from complex objects.
const apiResponse = {
data: {
user: {
id: 42,
profile: {
firstName: "Jane",
lastName: "Doe",
avatar: null
},
settings: {
theme: "dark"
}
}
},
meta: { requestId: "abc-123" }
};
const {
data: {
user: {
id: userId,
profile: { firstName, lastName, avatar = "default.png" },
settings: { theme, language = "en" }
}
},
meta: { requestId }
} = apiResponse;
console.log("User ID:", userId);
console.log("Name:", firstName, lastName);
console.log("Avatar:", avatar);
console.log("Theme:", theme);
console.log("Language:", language);
console.log("Request:", requestId);
User ID: 42
Name: Jane Doe
Avatar: null
Theme: dark
Language: en
Request: abc-123
Key Concept: Defaults only apply when the value is undefined, NOT null. Avatar is null so "default.png" does not activate. Renaming uses originalKey: newName syntax.
Problem 13 — Spread and Rest Patterns
Demonstrate practical uses of spread and rest operators for merging, copying, and collecting arguments.
const defaults = { color: "blue", size: "md", border: true };
const userPrefs = { color: "red", size: "lg" };
const config = { ...defaults, ...userPrefs };
console.log(config);
const { color, ...rest } = config;
console.log("Color:", color);
console.log("Rest:", rest);
function sum(first, ...others) {
return others.reduce((acc, n) => acc + n, first);
}
console.log(sum(1, 2, 3, 4, 5));
const original = [1, 2, 3];
const extended = [...original, 4, 5, ...original];
console.log(extended);
{ color: "red", size: "lg", border: true }
Color: red
Rest: { size: "lg", border: true }
15
[1, 2, 3, 4, 5, 1, 2, 3]Key Concept: Spread expands iterables into elements. Rest collects remaining elements into an array/object. In object merging, later properties override earlier ones.
Build utility functions for common string operations without relying on external libraries.
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
function camelCase(str) {
return str
.split(/[-_\s]+/)
.map((word, i) => (i === 0 ? word.toLowerCase() : capitalize(word)))
.join("");
}
function truncate(str, maxLen, suffix = "...") {
if (str.length <= maxLen) return str;
return str.slice(0, maxLen - suffix.length) + suffix;
}
function countOccurrences(str, sub) {
let count = 0;
let pos = 0;
while ((pos = str.indexOf(sub, pos)) !== -1) {
count++;
pos += sub.length;
}
return count;
}
console.log(capitalize("hELLO wORLD"));
console.log(camelCase("get-user-profile"));
console.log(camelCase("background_color_value"));
console.log(truncate("JavaScript is awesome", 15));
console.log(countOccurrences("banana", "an"));
Hello world
getUserProfile
backgroundColorValue
JavaScript i...
2
Key Concept: split, slice, indexOf, and charAt are the building blocks. Regex in split handles multiple delimiter types at once.
Problem 15 — Custom Sorting
Implement sorting comparators for complex data scenarios.
const products = [
{ name: "Laptop", price: 999, rating: 4.5 },
{ name: "Phone", price: 699, rating: 4.7 },
{ name: "Tablet", price: 499, rating: 4.2 },
{ name: "Watch", price: 299, rating: 4.7 },
{ name: "Headphones", price: 199, rating: 4.5 }
];
const sorted = [...products].sort((a, b) => {
if (b.rating !== a.rating) return b.rating - a.rating;
return a.price - b.price;
});
console.log("Multi-key sort:");
sorted.forEach(p => console.log(` ${p.name}: $${p.price} (${p.rating}★)`));
const files = ["file10.txt", "file2.txt", "file1.txt", "file20.txt"];
const naturalSorted = [...files].sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true })
);
console.log("\nNatural sort:", naturalSorted);
Multi-key sort:
Watch: $299 (4.7★)
Phone: $699 (4.7★)
Headphones: $199 (4.5★)
Laptop: $999 (4.5★)
Tablet: $499 (4.2★)
Natural sort: ["file1.txt", "file2.txt", "file10.txt", "file20.txt"]
Key Concept: The comparator returns negative (a first), positive (b first), or zero (equal). Multi-key sorting checks primary key first, then secondary. localeCompare with numeric: true handles embedded numbers.
Problem 16 — Binary Search Implementation
Implement binary search on a sorted array and return the index of the target element.
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
const sorted = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
console.log("Find 23:", binarySearch(sorted, 23));
console.log("Find 72:", binarySearch(sorted, 72));
console.log("Find 15:", binarySearch(sorted, 15));
console.log("Find 2:", binarySearch(sorted, 2));
Find 23: 5
Find 72: 8
Find 15: -1
Find 2: 0
Key Concept: Binary search halves the search space each iteration — O(log n). The array MUST be sorted. It compares the middle element to the target and eliminates half the remaining elements.
Problem 17 — Closures and Private State
Use closures to create objects with truly private variables that cannot be accessed externally.
function createCounter(initial = 0) {
let count = initial;
const history = [];
return {
increment(step = 1) {
count += step;
history.push({ action: "increment", value: count });
return count;
},
decrement(step = 1) {
count -= step;
history.push({ action: "decrement", value: count });
return count;
},
getCount() {
return count;
},
getHistory() {
return [...history];
},
reset() {
count = initial;
history.length = 0;
return count;
}
};
}
const counter = createCounter(10);
console.log(counter.increment());
console.log(counter.increment(5));
console.log(counter.decrement(3));
console.log("Count:", counter.getCount());
console.log("History:", JSON.stringify(counter.getHistory()));
console.log("After reset:", counter.reset());
11
16
13
Count: 13
History: [{"action":"increment","value":11},{"action":"increment","value":16},{"action":"decrement","value":13}]
After reset: 10Key Concept: count and history are enclosed within the factory function's scope. No external code can access them directly — only through the returned methods. This is the module pattern for encapsulation.
Problem 18 — Higher-Order Function Composition
Build a compose and pipe utility that chains multiple functions together.
function compose(...fns) {
return function (x) {
return fns.reduceRight((acc, fn) => fn(acc), x);
};
}
function pipe(...fns) {
return function (x) {
return fns.reduce((acc, fn) => fn(acc), x);
};
}
const double = x => x * 2;
const addTen = x => x + 10;
const square = x => x * x;
const composed = compose(square, addTen, double);
const piped = pipe(double, addTen, square);
console.log("compose(square, addTen, double)(3):", composed(3));
// double(3)=6 → addTen(6)=16 → square(16)=256
console.log("pipe(double, addTen, square)(3):", piped(3));
// double(3)=6 → addTen(6)=16 → square(16)=256
const processName = pipe(
str => str.trim(),
str => str.toLowerCase(),
str => str.split(" "),
parts => parts.map(p => p[0].toUpperCase() + p.slice(1)),
parts => parts.join(" ")
);
console.log(processName(" jOHN dOE "));
compose(square, addTen, double)(3): 256
pipe(double, addTen, square)(3): 256
John Doe
Key Concept: compose applies functions right-to-left, pipe applies left-to-right. Both transform a value through a chain of pure functions, enabling reusable code.
Problem 19 — Throttle Function
Implement a throttle that ensures a function runs at most once per time interval.
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
let logs = [];
const logEvent = throttle((msg) => logs.push(msg), 100);
logEvent("A");
logEvent("B");
logEvent("C");
setTimeout(() => { logEvent("D"); logEvent("E"); }, 150);
setTimeout(() => console.log("Logged events:", logs), 300);
Logged events: ["A", "D"]
Key Concept: Unlike debounce (which delays until quiet), throttle guarantees execution at fixed intervals. The first call executes immediately; calls within the interval are skipped.
Problem 20 — Implement Promise.all
Recreate Promise.all to understand concurrent promise resolution.
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
const total = promises.length;
if (total === 0) {
resolve([]);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(value => {
results[index] = value;
completed++;
if (completed === total) {
resolve(results);
}
}).catch(reject);
});
});
}
const p1 = Promise.resolve(1);
const p2 = new Promise(res => setTimeout(() => res(2), 50));
const p3 = Promise.resolve(3);
promiseAll([p1, p2, p3]).then(results => {
console.log("All resolved:", results);
});
promiseAll([p1, Promise.reject("Error!"), p3])
.then(results => console.log(results))
.catch(err => console.log("Caught:", err));
Caught: Error!
All resolved: [1, 2, 3]
Key Concept: Promise.all resolves when ALL promises resolve (maintaining order) and rejects when ANY rejects. index ensures results maintain positions regardless of resolution order.
Problem 21 — Event Emitter Pattern
Implement an event emitter with on, off, once, and emit.
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push({ fn: listener, once: false });
return this;
}
once(event, listener) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push({ fn: listener, once: true });
return this;
}
off(event, listener) {
if (!this.events[event]) return this;
this.events[event] = this.events[event].filter(l => l.fn !== listener);
return this;
}
emit(event, ...args) {
if (!this.events[event]) return false;
this.events[event] = this.events[event].filter(l => { l.fn(...args); return !l.once; });
return true;
}
}
const emitter = new EventEmitter();
const greet = name => console.log(`Hello, ${name}!`);
emitter.on("greet", greet);
emitter.once("greet", name => console.log(`Welcome, ${name}! (once)`));
console.log("--- First emit ---");
emitter.emit("greet", "Alice");
console.log("--- Second emit ---");
emitter.emit("greet", "Bob");
emitter.off("greet", greet);
console.log("--- Third emit (after off) ---");
emitter.emit("greet", "Charlie");
--- First emit ---
Hello, Alice!
Welcome, Alice! (once)
--- Second emit ---
Hello, Bob!
--- Third emit (after off) ---
Key Concept: Listeners are stored in a map by event name. once listeners are removed after first execution. off removes by reference equality. This is the observer pattern used in Node.js and DOM events.
Problem 22 — DOM Event Delegation Concept
Demonstrate event delegation using a simulated DOM structure.
class DOMSimulator {
constructor() {
this.handlers = {};
}
delegate(parentSelector, childSelector, event, handler) {
if (!this.handlers[parentSelector]) this.handlers[parentSelector] = [];
this.handlers[parentSelector].push({ childSelector, event, handler });
}
simulateClick(parentSelector, clickedElement) {
const delegates = this.handlers[parentSelector] || [];
for (const d of delegates) {
if (clickedElement.matches(d.childSelector))
d.handler({ target: clickedElement, type: d.event });
}
}
}
const dom = new DOMSimulator();
dom.delegate("#todo-list", ".todo-item", "click", (e) => {
console.log(`Clicked: ${e.target.textContent}`);
});
dom.delegate("#todo-list", ".delete-btn", "click", (e) => {
console.log(`Deleting: ${e.target.dataset.id}`);
});
dom.simulateClick("#todo-list", { matches: s => s === ".todo-item", textContent: "Buy groceries" });
dom.simulateClick("#todo-list", { matches: s => s === ".delete-btn", dataset: { id: "task-3" } });
dom.simulateClick("#todo-list", { matches: s => s === ".unrelated", textContent: "Ignored" });
Clicked: Buy groceries
Deleting: task-3
Key Concept: Event delegation attaches one listener to a parent instead of many to children. It checks if the event target matches the desired selector. This handles dynamically added elements and reduces memory usage.
Practice Guidelines
- Solve without looking — Cover the solution and write your own version first.
- Test edge cases — Empty arrays, null values, single elements, large inputs.
- Explain complexity — State the time and space complexity of each solution.
- Refactor — Try an alternative approach (iterative vs recursive).
- Combine concepts — Chain techniques (destructure + reduce + closure).