LeetCode 2804 - Array Prototype ForEach
This problem asks us to implement a custom version of the forEach method for arrays in JavaScript. The goal is to extend the Array.prototype so that any array can call forEach(callback, context) and execute the callback on each element.
Difficulty: 🟢 Easy
Topics: —
Solution
Problem Understanding
This problem asks us to implement a custom version of the forEach method for arrays in JavaScript. The goal is to extend the Array.prototype so that any array can call forEach(callback, context) and execute the callback on each element. The callback function receives three parameters: the current element value, its index, and the array itself. Additionally, the optional context argument should be used to bind the this keyword inside the callback.
The input consists of a valid array and a function, along with a context object. The expected output is that the array is mutated (or the callback is executed) for each element; the forEach method itself does not return a value. Constraints indicate that the array length can be large (up to 100,000 elements), so performance should be linear. Edge cases include empty arrays, arrays with mixed types, and using a null or empty context.
Key points to note are that we cannot use built-in array methods like forEach to implement this, and we must ensure the this binding works correctly.
Approaches
The brute-force approach is straightforward: manually loop over each element in the array and call the callback with the element, index, and array, while using Function.prototype.call to bind the context. This approach is both simple and sufficient because iterating linearly over the array is efficient for the constraints. There is no faster asymptotic solution because each element must be visited once.
The key insight is that the problem is essentially a linear iteration with context binding. By using a standard for loop and callback.call(context, ...), we achieve the correct behavior without relying on built-in array iteration methods.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(1) | Iterate manually, call callback on each element with context |
| Optimal | O(n) | O(1) | Same as brute force; optimal given requirement to process each element |
Algorithm Walkthrough
- Extend
Array.prototypewith a new method namedforEach. - The method accepts a
callbackfunction and an optionalcontextobject. - Start a
forloop from index0tothis.length - 1to iterate over the array. - On each iteration, invoke
callback.call(context, currentValue, index, this). This ensures thecallbackexecutes with the correctthisvalue. - The method does not return anything.
- After the loop completes, the array has been processed according to the callback logic.
Why it works: The loop guarantees that each element is visited exactly once in order. Using call ensures the correct binding of this to the provided context. The invariant is that after each iteration, the callback has been executed for the corresponding index.
Python Solution
# Python equivalent for testing, though Python does not modify Array.prototype
from typing import Any, Callable, List
def forEach(arr: List[Any], callback: Callable[[Any, int, List[Any]], None], context: Any) -> None:
for i in range(len(arr)):
callback(arr[i], i, arr)
Explanation: In Python, we define a standalone forEach function since Python lists do not have a prototype. The function loops over the array and calls the callback with the element, index, and array. Python does not have a direct equivalent of this, so context is ignored, but in JavaScript it would be applied using call.
Go Solution
package main
type Any interface{}
func ForEach(arr []Any, callback func(val Any, index int, arr []Any), context Any) {
for i := 0; i < len(arr); i++ {
callback(arr[i], i, arr)
}
}
Explanation: Go does not have prototypes or this binding, so the context is unused. We simply loop over the slice and execute the callback with each element and index. This mirrors the linear iteration of JavaScript.
Worked Examples
Example 1
Input: [1,2,3], callback multiplies by 2, context {"context": true}
Iteration table:
| Index | Value | Callback result | Array state |
|---|---|---|---|
| 0 | 1 | 2 | [2,2,3] |
| 1 | 2 | 4 | [2,4,3] |
| 2 | 3 | 6 | [2,4,6] |
Example 2
Input: [true,true,false,false], callback sets value to this, context {"context": false}
| Index | Value | Callback result | Array state |
|---|---|---|---|
| 0 | true | {"context": false} | [{"context": false},true,false,false] |
| 1 | true | {"context": false} | [{"context": false},{"context": false},false,false] |
| 2 | false | {"context": false} | [{"context": false},{"context": false},{"context": false},false] |
| 3 | false | {"context": false} | [{"context": false},{"context": false},{"context": false},{"context": false}] |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element is visited exactly once |
| Space | O(1) | No additional space besides loop variables |
The complexity is optimal because every element must be processed. Only constant extra space is required.
Test Cases
# test cases
def test_forEach():
arr1 = [1,2,3]
forEach(arr1, lambda val, i, arr: arr.__setitem__(i, val*2), {"context": True})
assert arr1 == [2,4,6] # multiply by 2
arr2 = [True, True, False, False]
ctx = {"context": False}
forEach(arr2, lambda val, i, arr: arr.__setitem__(i, ctx), ctx)
assert arr2 == [ctx, ctx, ctx, ctx] # set context
arr3 = [True, True, False, False]
forEach(arr3, lambda val, i, arr: arr.__setitem__(i, not val), {"context": 5})
assert arr3 == [False, False, True, True] # logical negation
arr4 = []
forEach(arr4, lambda val, i, arr: arr.__setitem__(i, val*2), {})
assert arr4 == [] # empty array
test_forEach()
| Test | Why |
|---|---|
[1,2,3] |
tests numerical transformation |
[True, True, False, False] with context |
tests correct this binding |
| empty array | tests edge case of no iteration |
| logical negation | tests boolean transformation |
Edge Cases
Empty Array: When arr is empty, the loop does not execute, and the array remains empty. This ensures no errors occur when the input is size 0.
Null or Undefined Context: If context is None or not provided, callback.call in JavaScript defaults this to undefined. Our implementation handles this by passing whatever is provided.
Mixed Data Types: Arrays may contain numbers, booleans, or objects. The callback receives the value as-is, and the implementation does not assume a type. This prevents type errors during iteration.
Very Large Arrays: With arr.length up to 100,000, the linear iteration ensures the implementation remains efficient and avoids stack overflows, as recursion is not used.