Facebook Pixel

2804. Array Prototype ForEach 🔒

Problem Description

This problem asks you to implement a custom forEach method for JavaScript/TypeScript arrays. The goal is to extend the Array prototype so that any array can call array.forEach(callback, context).

The method should iterate through each element of the array and execute a callback function for each element. The callback function receives three arguments:

  • currentValue: The current element being processed
  • index: The index of the current element in the array
  • array: A reference to the entire array

Additionally, the method accepts an optional context parameter that determines what this refers to inside the callback function. When the context is provided, the callback should be executed with that context as its this value.

Key requirements:

  • The forEach method should not return any value (void return type)
  • You should implement this without using built-in array methods
  • The callback should be invoked for each element in the array in order from index 0 to length-1

For example, if you have an array [1, 2, 3] and a callback that doubles each value, after calling forEach, the array would be modified to [2, 4, 6]. The context object allows you to control the execution context of the callback function.

The solution uses a simple for loop to iterate through the array and callback.call(context, ...) to invoke the callback with the specified context and arguments for each element.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is understanding how JavaScript's built-in forEach works under the hood. At its core, forEach is simply a loop that visits each element in an array and executes a function for each one.

To implement this, we need to think about the basic mechanics:

  1. We need to traverse the array from start to end
  2. For each element, we need to invoke the callback function
  3. The callback needs access to three pieces of information: the current element, its index, and the full array

The straightforward approach is to use a for loop with an index counter i that goes from 0 to this.length - 1. At each iteration, we have:

  • The current element: this[i]
  • The current index: i
  • The array itself: this (since we're extending the Array prototype)

The trickier part is handling the context parameter. In JavaScript, when you call a function normally like callback(), the this inside that function might not be what you expect. The context parameter allows users to explicitly set what this should be inside the callback.

This is where callback.call() comes in. The call() method lets us invoke a function while explicitly setting its this value. The first argument to call() becomes the this value inside the function, and subsequent arguments are passed as regular parameters.

So callback.call(context, this[i], i, this) means:

  • Execute callback
  • Set its this to context
  • Pass this[i] as the first argument (currentValue)
  • Pass i as the second argument (index)
  • Pass this as the third argument (array)

This mirrors exactly how the native forEach behaves, allowing the callback to access all necessary information while respecting the provided execution context.

Solution Approach

The implementation follows a straightforward iterative approach using a traditional for loop to traverse the array elements.

Implementation Steps:

  1. Method Extension: We extend Array.prototype to add our custom forEach method that accepts two parameters:

    • callback: The function to execute for each element
    • context: The optional object to use as this when executing the callback
  2. Loop Structure: We use a standard for loop with index i starting from 0 and continuing while i < this.length:

    for (let i = 0; i < this.length; ++i)

    Here, this refers to the array instance on which forEach is called.

  3. Callback Invocation: For each iteration, we invoke the callback using the call method:

    callback.call(context, this[i], i, this)

    This ensures:

    • The callback's this value is set to the provided context
    • The callback receives three arguments in order:
      • this[i]: The current element value
      • i: The current index
      • this: The entire array reference
  4. No Return Value: The method completes without returning anything (void), as specified by the problem requirements.

Key Design Decisions:

  • Using ++i (pre-increment) instead of i++ is a minor optimization that avoids creating a temporary variable, though the difference is negligible in modern JavaScript engines.

  • The call method is essential here because it allows us to control the execution context. If we simply called callback(this[i], i, this), the this inside the callback would be undefined (in strict mode) or the global object (in non-strict mode), not the desired context.

  • We iterate through all indices from 0 to length - 1, which handles both dense and sparse arrays correctly, executing the callback even for undefined elements (matching native forEach behavior).

The entire implementation is concise yet complete, requiring only a single loop and one function invocation per element, resulting in O(n) time complexity where n is the array length.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to see how our custom forEach implementation works.

Example Setup:

// Define our custom forEach
Array.prototype.forEach = function(callback, context) {
    for (let i = 0; i < this.length; ++i) {
        callback.call(context, this[i], i, this);
    }
};

// Create a sample array and context object
const numbers = [10, 20, 30];
const multiplier = {
    factor: 2,
    results: []
};

// Define a callback that uses the context
function multiplyAndStore(value, index, array) {
    // 'this' refers to the multiplier object
    const result = value * this.factor;
    this.results.push(`Index ${index}: ${value} * ${this.factor} = ${result}`);
}

// Execute forEach with context
numbers.forEach(multiplyAndStore, multiplier);

Step-by-step execution:

Iteration 1 (i = 0):

  • Current element: this[0] = 10
  • Execute: callback.call(multiplier, 10, 0, [10, 20, 30])
  • Inside callback:
    • this = multiplier object
    • value = 10, index = 0, array = [10, 20, 30]
    • Calculates: 10 * 2 = 20
    • Pushes to results: "Index 0: 10 * 2 = 20"

Iteration 2 (i = 1):

  • Current element: this[1] = 20
  • Execute: callback.call(multiplier, 20, 1, [10, 20, 30])
  • Inside callback:
    • this = multiplier object
    • value = 20, index = 1, array = [10, 20, 30]
    • Calculates: 20 * 2 = 40
    • Pushes to results: "Index 1: 20 * 2 = 40"

Iteration 3 (i = 2):

  • Current element: this[2] = 30
  • Execute: callback.call(multiplier, 30, 2, [10, 20, 30])
  • Inside callback:
    • this = multiplier object
    • value = 30, index = 2, array = [10, 20, 30]
    • Calculates: 30 * 2 = 60
    • Pushes to results: "Index 2: 30 * 2 = 60"

Final Result:

console.log(multiplier.results);
// Output: [
//   "Index 0: 10 * 2 = 20",
//   "Index 1: 20 * 2 = 40", 
//   "Index 2: 30 * 2 = 60"
// ]

The key insight here is how callback.call(context, ...) allows the callback function to access properties and methods of the multiplier object through this. Without the context parameter, this.factor and this.results would be undefined, causing the callback to fail. This demonstrates why the call method is essential for properly implementing forEach with context support.

Solution Implementation

1class Array(list):
2    """
3    Custom Array class extending list with forEach method implementation
4    """
5  
6    def forEach(self, callback, context=None):
7        """
8        Custom implementation of JavaScript's Array.prototype.forEach method
9        Iterates through array elements and executes a callback function for each element
10      
11        Args:
12            callback: Function to execute for each element
13            context: Optional context object to bind as 'this' when executing callback
14        """
15        # Iterate through each element in the array
16        for i in range(len(self)):
17            # Execute callback with specified context, passing current element, index, and array
18            if context is not None:
19                # Bind context as 'self' for the callback if provided
20                callback(self[i], i, self)
21            else:
22                # Execute callback without specific context binding
23                callback(self[i], i, self)
24
25
26# Example usage demonstration
27if __name__ == "__main__":
28    # Create an Array instance with initial values
29    arr = Array([1, 2, 3])
30  
31    # Define callback function that doubles each element
32    def callback(val, i, arr):
33        """Callback function that modifies array elements in place"""
34        arr[i] = val * 2
35  
36    # Context object (not directly used in Python version as Python doesn't have 'this' binding)
37    context = {"context": True}
38  
39    # Execute forEach with callback and context
40    arr.forEach(callback, context)
41  
42    # Display the modified array
43    print(arr)  # Output: [2, 4, 6]
44
1/**
2 * Custom implementation of forEach method for arrays
3 * Iterates through array elements and executes a callback function for each element
4 */
5public class ArrayForEach {
6  
7    /**
8     * Functional interface representing a callback function for forEach operation
9     * @param <T> The type of elements in the array
10     */
11    @FunctionalInterface
12    public interface ForEachCallback<T> {
13        /**
14         * Executes the callback logic for each element
15         * @param element The current element being processed
16         * @param index The index of the current element
17         * @param array The array being iterated
18         */
19        void call(T element, int index, T[] array);
20    }
21  
22    /**
23     * Custom forEach implementation for generic arrays
24     * @param array The array to iterate through
25     * @param callback The callback function to execute for each element
26     * @param context The context object (not directly used in Java implementation)
27     * @param <T> The type of elements in the array
28     */
29    public static <T> void forEach(T[] array, ForEachCallback<T> callback, Object context) {
30        // Iterate through each element in the array
31        for (int i = 0; i < array.length; i++) {
32            // Execute callback, passing current element, index, and array
33            callback.call(array[i], i, array);
34        }
35    }
36  
37    /**
38     * Example usage demonstrating the forEach method
39     */
40    public static void main(String[] args) {
41        // Initialize an array with sample values
42        Integer[] arr = {1, 2, 3};
43      
44        // Define the callback function that doubles each element
45        ForEachCallback<Integer> callback = (val, i, array) -> {
46            // Multiply each element by 2 and update the array
47            array[i] = val * 2;
48        };
49      
50        // Create a context object (for compatibility with original signature)
51        Object context = new Object();
52      
53        // Execute forEach with the callback
54        forEach(arr, callback, context);
55      
56        // Print the modified array
57        System.out.print("Output: [");
58        for (int i = 0; i < arr.length; i++) {
59            System.out.print(arr[i]);
60            if (i < arr.length - 1) {
61                System.out.print(", ");
62            }
63        }
64        System.out.println("]"); // Output: [2, 4, 6]
65    }
66}
67
1#include <vector>
2#include <functional>
3
4/**
5 * Custom implementation of forEach method for vectors
6 * Iterates through vector elements and executes a callback function for each element
7 * 
8 * @tparam T The type of elements in the vector
9 * @param callback Function to execute for each element, takes (element, index, vector reference)
10 * @param context Optional context object (can be nullptr)
11 */
12template<typename T>
13void forEach(std::vector<T>& vec, 
14             std::function<void(T&, size_t, std::vector<T>&)> callback, 
15             void* context = nullptr) {
16    // Iterate through each element in the vector
17    for (size_t i = 0; i < vec.size(); ++i) {
18        // Execute callback, passing current element by reference, index, and vector reference
19        callback(vec[i], i, vec);
20    }
21}
22
23/**
24 * Example usage:
25 * 
26 * int main() {
27 *     std::vector<int> arr = {1, 2, 3};
28 *     
29 *     // Define callback function that doubles each element
30 *     auto callback = [](int& val, size_t i, std::vector<int>& arr) {
31 *         arr[i] = val * 2;
32 *     };
33 *     
34 *     // Context object (optional, not used in this example)
35 *     void* context = nullptr;
36 *     
37 *     // Apply forEach to the vector
38 *     forEach(arr, callback, context);
39 *     
40 *     // Print result: [2, 4, 6]
41 *     for (const auto& elem : arr) {
42 *         std::cout << elem << " ";
43 *     }
44 *     
45 *     return 0;
46 * }
47 */
48
1/**
2 * Custom implementation of Array.prototype.forEach method
3 * Iterates through array elements and executes a callback function for each element
4 */
5Array.prototype.forEach = function (callback: Function, context: any): void {
6    // Iterate through each element in the array
7    for (let i = 0; i < this.length; ++i) {
8        // Execute callback with specified context, passing current element, index, and array
9        callback.call(context, this[i], i, this);
10    }
11};
12
13/**
14 * Example usage:
15 * 
16 * const arr = [1, 2, 3];
17 * const callback = (val: number, i: number, arr: number[]) => arr[i] = val * 2;
18 * const context = { "context": true };
19 * 
20 * arr.forEach(callback, context);
21 * 
22 * console.log(arr); // Output: [2, 4, 6]
23 */
24

Time and Space Complexity

Time Complexity: O(n) where n is the length of the array. The implementation uses a single for loop that iterates through each element of the array exactly once, calling the callback function for each element. Each callback invocation is assumed to be O(1) for basic operations, though the actual complexity depends on the callback implementation.

Space Complexity: O(1) for the forEach implementation itself. The method only uses a constant amount of extra space for the loop counter variable i. No additional data structures are created that scale with the input size. However, the total space complexity can vary depending on what the callback function does internally - if the callback creates new objects or data structures, that would add to the space complexity, but the forEach method itself maintains constant space usage.

Common Pitfalls

1. Modifying Array Length During Iteration

One of the most common pitfalls when implementing forEach is not handling cases where the callback function modifies the array's length during iteration. This can lead to skipped elements or accessing undefined indices.

Problem Example:

const arr = [1, 2, 3, 4, 5];
arr.forEach((val, i, array) => {
    if (val === 2) {
        array.push(6); // Adding elements during iteration
    }
    console.log(val);
});

In the basic implementation, the loop condition i < this.length is evaluated on each iteration, so adding elements will extend the loop, potentially causing infinite loops. Removing elements might skip some elements.

Solution: Cache the initial array length before starting the iteration:

Array.prototype.forEach = function(callback, context) {
    const len = this.length; // Cache initial length
    for (let i = 0; i < len; ++i) {
        if (i in this) { // Check if index still exists
            callback.call(context, this[i], i, this);
        }
    }
};

2. Not Handling Sparse Arrays Correctly

JavaScript arrays can be sparse (have "holes" where indices are missing). The native forEach skips these holes, but a naive implementation might not.

Problem Example:

const sparseArr = [1, , 3]; // Index 1 is a hole
sparseArr.forEach(val => console.log(val)); 
// Should only log: 1, 3 (not undefined for index 1)

Solution: Use the in operator to check if an index actually exists:

Array.prototype.forEach = function(callback, context) {
    for (let i = 0; i < this.length; ++i) {
        if (i in this) { // Only process existing indices
            callback.call(context, this[i], i, this);
        }
    }
};

3. Incorrect Context Binding in Arrow Functions

When the callback is an arrow function, attempting to bind a context won't work as arrow functions have lexically bound this that cannot be changed.

Problem Example:

const context = { multiplier: 2 };
[1, 2, 3].forEach((val) => {
    console.log(this.multiplier * val); // 'this' won't be 'context'
}, context);

Solution: Document this behavior and suggest using regular functions when context binding is needed:

// Use regular function for context binding
[1, 2, 3].forEach(function(val) {
    console.log(this.multiplier * val); // Works correctly
}, context);

4. Not Validating Callback Parameter

Failing to check if the callback is actually a function can cause runtime errors.

Problem Example:

[1, 2, 3].forEach("not a function"); // Should throw TypeError

Solution: Add type validation:

Array.prototype.forEach = function(callback, context) {
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }
  
    for (let i = 0; i < this.length; ++i) {
        if (i in this) {
            callback.call(context, this[i], i, this);
        }
    }
};

5. Performance Issues with Large Arrays

Using this.length directly in the loop condition causes property lookup on each iteration, which can impact performance for very large arrays.

Solution: Cache the length value:

Array.prototype.forEach = function(callback, context) {
    const len = this.length;
    for (let i = 0; i < len; ++i) {
        if (i in this) {
            callback.call(context, this[i], i, this);
        }
    }
};

These pitfalls highlight the importance of considering edge cases and following the native implementation's behavior closely when creating polyfills or custom implementations of built-in methods.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More