LeetCode 2693 - Call Function with Custom Context

This problem asks us to implement a custom version of JavaScript’s Function.prototype.call method, called callPolyfill. The purpose is to execute a function with an explicit this context. Normally, in JavaScript, this depends on how a function is called.

LeetCode Problem 2693

Difficulty: 🟡 Medium
Topics:

Solution

Problem Understanding

This problem asks us to implement a custom version of JavaScript’s Function.prototype.call method, called callPolyfill. The purpose is to execute a function with an explicit this context. Normally, in JavaScript, this depends on how a function is called. Using call, we can override it. Here, the challenge is to implement similar functionality without using the built-in Function.call method.

The input consists of a function fn and an array of arguments args. The first element of args is always an object which should be used as the this context. The remaining elements are the arguments to pass to the function. The expected output is whatever the function returns when called with this this context and arguments.

Constraints guarantee that the first argument is always a non-null object, so we do not need to handle primitives or null for this. The number of arguments is manageable, up to 100, and the object can be relatively large.

Important edge cases to consider include functions that do not use this, functions with zero arguments besides this, and objects with properties that might shadow built-in Object properties. Our solution should handle all valid JavaScript objects.

Approaches

The brute-force approach is to dynamically attach the function to the object as a temporary property and then invoke it. This works because when a function is a property of an object and invoked as obj.fn(), its this automatically refers to obj. The drawback is that we need to carefully handle the temporary property to avoid collisions and ensure it is cleaned up afterward. Despite being straightforward, this is actually the optimal approach here because it avoids the forbidden Function.call and executes in constant time relative to the arguments.

The key insight is that JavaScript functions are first-class objects and their context is determined by how they are invoked. By attaching the function temporarily to the object and calling it, we simulate the effect of call. This method works for any number of arguments because JavaScript’s spread operator allows passing them dynamically.

Approach Time Complexity Space Complexity Notes
Brute Force O(n) O(1) Attach function to object, call it, remove property; linear in number of arguments
Optimal O(n) O(1) Same as brute-force, efficient and handles all constraints

Algorithm Walkthrough

  1. Extract the first argument from args, which is the object to be used as this.
  2. Extract the remaining arguments to pass to the function.
  3. Create a unique symbol or property key to temporarily attach the function to the object. Using a symbol avoids overwriting existing properties.
  4. Assign the function to this temporary property on the object.
  5. Call the function as a method of the object using the spread operator to pass all remaining arguments.
  6. Delete the temporary property to restore the object to its original state.
  7. Return the result of the function call.

This works because JavaScript sets this based on the caller object. By temporarily making the function a method of the desired object, we guarantee the correct context. Using a symbol ensures we do not accidentally overwrite existing properties.

Python Solution

from typing import Any, Callable

def callPolyfill(self: Callable, *args: Any) -> Any:
    obj, *func_args = args
    if not isinstance(obj, dict):
        raise TypeError("First argument must be a non-null object")
    temp_key = "_temp_fn"
    while temp_key in obj:
        temp_key += "_"
    obj[temp_key] = self
    try:
        # simulate method call
        return obj[temp_key](*func_args)
    finally:
        del obj[temp_key]

# Monkey patch Function
Callable.callPolyfill = callPolyfill

In this implementation, we unpack the first argument as the this object and the rest as function arguments. We assign the function to a temporary key in the object, call it, and remove the key. The finally block ensures cleanup even if the function raises an exception.

Go Solution

package main

import "reflect"

func CallPolyfill(fn interface{}, args ...interface{}) interface{} {
    if len(args) == 0 {
        panic("First argument must be an object")
    }
    obj := args[0]
    fnArgs := args[1:]
    fnValue := reflect.ValueOf(fn)
    callArgs := make([]reflect.Value, len(fnArgs))
    for i, a := range fnArgs {
        callArgs[i] = reflect.ValueOf(a)
    }
    return fnValue.Call(callArgs)[0].Interface()
}

In Go, we simulate the function call using reflect. Since Go does not have dynamic this binding, the first argument is treated as a normal parameter. We construct a slice of reflect.Value for the remaining arguments, call the function reflectively, and return the first element as the result. This differs from Python and JS because Go functions do not have a this context.

Worked Examples

Example 1

fn = function add(b) { return this.a + b; }

args = [{"a": 5}, 7]

Step 1: Extract obj = {"a": 5}, func_args = [7]

Step 2: Attach fn to obj temporarily.

Step 3: Call obj.temp_fn(7) → evaluates to 5 + 7 = 12

Step 4: Remove temporary key.

Result → 12

Example 2

fn = function tax(price, taxRate) { return "The cost of " + this.item + " is " + (price * taxRate); }

args = [{"item": "burger"}, 10, 1.1]

Step 1: Extract obj = {"item": "burger"}, func_args = [10, 1.1]

Step 2: Attach fn to obj temporarily

Step 3: Call obj.temp_fn(10, 1.1)"The cost of burger is 11"

Step 4: Remove temporary key

Result → "The cost of the burger is 11"

Complexity Analysis

Measure Complexity Explanation
Time O(n) Function call takes O(n) for n arguments to unpack and spread
Space O(1) Temporary key is a constant overhead; no additional data structures proportional to input

The algorithm is efficient because it only involves temporary object property manipulation and a single function call.

Test Cases

# test cases
def test_callPolyfill():
    fn1 = lambda self, b: self['a'] + b
    assert callPolyfill(fn1, {"a": 5}, 7) == 12  # basic addition with this

    def tax(price, taxRate):
        return f"The cost of the {self['item']} is {price * taxRate}"
    assert callPolyfill(lambda price, taxRate: price*taxRate, {"item":"burger"}, 10, 1.1) == 11  # multiplication test

    fn2 = lambda self: "no args"
    assert callPolyfill(fn2, {"key": "value"}) == "no args"  # function with no additional arguments

    fn3 = lambda self, x, y, z: self['v'] + x + y + z
    assert callPolyfill(fn3, {"v": 1}, 2, 3, 4) == 10  # multiple arguments

    fn4 = lambda self: self.get('a', 0)
    assert callPolyfill(fn4, {"b": 1}) == 0  # object without expected key
Test Why
Basic addition with this Validates correct this binding
Multiplication with this Validates numeric operations with object context
Function with no extra arguments Ensures edge case of single-argument function works
Multiple arguments Ensures spreading multiple arguments works
Object without key Ensures missing properties handled correctly

Edge Cases

One edge case is when the object already has a property named _temp_fn. Our implementation handles this by generating a unique temporary key by appending underscores until there is no collision.

Another edge case is when the function does not use this at all. Our implementation still correctly passes arguments, and the unused this does not cause any errors.

A third edge case is when the function throws an exception. The use of the finally block ensures that the temporary property is always cleaned up, preventing side effects on the object.

This careful handling guarantees that the implementation is robust for all valid inputs under the problem constraints.