Practice Questions
1. What is a higher-order function? Give two examples of built-in JavaScript array methods that are higher-order functions and explain what each one does.
Solution
A higher-order function is a function that either takes one or more functions as arguments or returns a function. Two examples:
forEach: Takes a callback function and calls it once for each element in the array. It does not return a new array.filter: Takes a callback function that returns a boolean. It returns a new array containing only the elements for which the callback returnedtrue.
Other valid examples include map and reduce.
2. Explain the difference between forEach and map. Write a short code example that uses map to transform an array of numbers into an array of their squares.
Solution
forEach iterates over each element and executes a callback for its side effects; it returns undefined. map also iterates over each element, but it returns a new array where each element is the result of the callback.
const numbers = [1, 2, 3, 4, 5];
const squares = numbers.map((n) => n * n);
console.log(squares); // [1, 4, 9, 16, 25]
3. What does the filter method return, and does it modify the original array? Write a function that takes an array of objects with a status property and returns only those with status equal to "active".
Solution
filter returns a new array containing elements that pass the test implemented by the callback. It does not modify the original array.
function getActiveItems(items) {
return items.filter((item) => item.status === "active");
}
const items = [
{ name: "A", status: "active" },
{ name: "B", status: "completed" },
{ name: "C", status: "active" },
];
console.log(getActiveItems(items));
// [{ name: "A", status: "active" }, { name: "C", status: "active" }]
4. Explain what a closure is using the following code. Why does the returned object’s add and getAll methods still have access to items after createList has finished executing? What happens if you call createList twice — do the two lists share the same items array?
function createList() {
const items = [];
return {
add(item) {
items.push(item);
},
getAll() {
return [...items];
},
};
}
Solution
A closure is created when a function retains access to variables from its enclosing scope even after the outer function has finished executing. The inner functions add and getAll “close over” the items variable. They hold a reference to it, which keeps it alive in memory.
When createList() returns, the items array is not garbage collected because the returned methods still reference it. Every call to add pushes into that same array, and getAll reads from it.
If you call createList twice, each call creates a new items array and a new set of closures over it. The two lists are completely independent:
const listA = createList();
const listB = createList();
listA.add("apple");
listB.add("banana");
console.log(listA.getAll()); // ["apple"]
console.log(listB.getAll()); // ["banana"]
Each call to createList creates its own scope with its own items, so the two lists do not interfere with each other.
5. Explain what immutability means in the context of functional programming. Given the following array, write code that adds a new item and code that updates an existing item’s done property to true — both without mutating the original array.
const tasks = [
{ id: 1, text: "Read", done: false },
{ id: 2, text: "Write", done: false },
];
Solution
Immutability means that once data is created, it is never changed. Instead of modifying an existing data structure, you create a new one with the changes you want. This makes the code more predictable and easier to reason about.
// Adding a new item (using spread operator)
const withNewTask = [...tasks, { id: 3, text: "Review", done: false }];
// Updating an existing item (using map)
const withUpdated = tasks.map((task) =>
task.id === 1 ? { ...task, done: true } : task,
);
// Original array is unchanged
console.log(tasks[0].done); // false
6. Define a Book class with the following requirements: private fields for title and pageCount, a constructor that accepts both values, getter methods for both fields, and a setter for pageCount that rejects negative values by throwing an error.
Solution
class Book {
#title;
#pageCount;
constructor(title, pageCount) {
this.#title = title;
this.pageCount = pageCount; // assign through the setter to validate
}
get title() {
return this.#title;
}
get pageCount() {
return this.#pageCount;
}
set pageCount(value) {
if (value < 0) {
throw new Error("Page count cannot be negative");
}
this.#pageCount = value;
}
}
const book = new Book("JavaScript Guide", 350);
console.log(book.title); // "JavaScript Guide"
book.pageCount = 400; // works
// book.pageCount = -10; // throws Error
7. What is a static method on a class, and when does it make sense to use one instead of an instance method? Write a Temperature class that stores a value in Celsius and has a static method fromFahrenheit(f) that creates a Temperature instance by converting from Fahrenheit.
Solution
A static method belongs to the class itself rather than to any particular instance. It is called on the class name (e.g., Temperature.fromFahrenheit()), not on an instance. Static methods are useful for factory functions, utility operations, and anything that belongs to the class as a whole but does not need data from a particular instance.
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
display() {
return `${this.celsius}°C`;
}
static fromFahrenheit(f) {
const celsius = (f - 32) * (5 / 9);
return new Temperature(celsius);
}
}
const boiling = new Temperature(100);
console.log(boiling.display()); // "100°C"
const bodyTemp = Temperature.fromFahrenheit(98.6);
console.log(bodyTemp.display()); // "37°C"
fromFahrenheit makes sense as a static method because it creates a new instance rather than operating on an existing one. Calling boiling.fromFahrenheit(212) would not mean anything, because the method’s job is to produce a Temperature, not to modify one.
8. The following import statement causes an error. Explain why it fails and write the corrected version. Then explain the general rule that distinguishes how default exports and named exports are imported.
// utils.js
export default function formatDate(date) {
/* ... */
}
export const LOCALE = "en-US";
// app.js
import { formatDate, LOCALE } from "./utils.js"; // Error!
Solution
The error occurs because formatDate is a default export, but the import uses curly braces { formatDate }, which is the syntax for named exports. Default exports are imported without braces; named exports require braces.
Corrected import:
import formatDate, { LOCALE } from "./utils.js";
The general rule:
- Default export: imported without curly braces, and you can name it anything, as in
import myFunc from "./utils.js". - Named export: imported with curly braces, and the name must match, or be aliased with
as, as inimport { LOCALE } from "./utils.js".
A module can have at most one default export but any number of named exports. When a module has a single primary value, such as a class or a main function, make it the default. If the module provides several related utilities, use named exports for all of them.
9. What is event delegation and why is it useful? Describe a scenario where adding an event listener to a parent element is more practical than adding one to each child element.
Solution
Event delegation is a pattern where you attach a single event listener to a parent element instead of attaching individual listeners to each child element. When an event occurs on a child, it bubbles up to the parent, where the listener handles it.
It is useful when:
- Child elements are created dynamically (e.g., items in a list that grows over time). If you attach listeners directly, newly added children would not have listeners.
- There are many child elements and attaching a listener to each one would be inefficient.
For example, if you have a list where users can click on any item, you can attach a single click listener to the <ul> element. Inside the handler, you check event.target to determine which <li> was clicked and respond accordingly.
const list = document.querySelector("ul");
list.addEventListener("click", (event) => {
if (event.target.tagName === "LI") {
console.log("Clicked:", event.target.textContent);
}
});
10. Compare the structured (procedural) and functional approaches to filtering an array. Given an array of numbers, write both a for loop version and a filter version that return only the even numbers. What are the trade-offs between the two approaches?
Solution
Structured (procedural) approach using a for loop:
const numbers = [1, 2, 3, 4, 5, 6];
const evens = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evens.push(numbers[i]);
}
}
Functional approach using filter:
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter((n) => n % 2 === 0);
Trade-offs:
- The
forloop is more explicit and gives full control over iteration (e.g., you can break early). It is familiar to programmers from most languages. - The
filterapproach is shorter and declarative. It says what you want rather than how to get it. You do not manage an index yourself, and it produces a new array without mutating the original. - The procedural approach mutates state, such as pushing to
evens, and that can introduce bugs in larger programs. The functional approach encourages immutability.
11. JavaScript uses callback functions everywhere — in array.map(fn), addEventListener("click", fn), setTimeout(fn, ms), and more. What property of JavaScript functions makes all of these possible? Explain using one of these examples to show why the pattern would not work if functions were not treated this way.
Solution
JavaScript functions are first-class values. They can be stored in variables, passed as arguments to other functions, and returned from functions. This is what makes callbacks possible.
Consider addEventListener:
function handleClick() {
console.log("Button was clicked");
}
button.addEventListener("click", handleClick);
Here, handleClick is passed as an argument to addEventListener. The browser stores this function reference internally and calls it later when the click event fires. This only works because functions are values. You can pass a function to another function the same way you pass a string or a number.
If functions were not first-class (as in some older languages), you would not be able to pass handleClick as an argument. You would need a completely different mechanism to tell the browser what code to run on click, such as a special syntax or a string of code to evaluate.
The same thing is going on in array.map(fn), where the array calls fn on each element, and in setTimeout(fn, ms), where the runtime calls fn after the delay. Almost every asynchronous pattern in JavaScript works this way.
12. Why does JavaScript use # syntax for private fields in classes instead of a keyword like private? What happens if you try to access a private field from outside the class?
Solution
JavaScript uses the # prefix for private fields to provide true privacy at the language level. Unlike conventions (such as prefixing with an underscore), the # syntax is enforced by the JavaScript engine. Private fields are not accessible outside the class body at all.
If you try to access a private field from outside the class, you get a SyntaxError:
class Person {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const p = new Person("Alice");
console.log(p.getName()); // "Alice"
// console.log(p.#name); // SyntaxError: Private field '#name'
// // must be declared in an enclosing class
This enforces encapsulation. External code has to go through the public interface (getters, setters, and methods) to work with the object’s data, which protects the object’s invariants and makes the code easier to maintain.