Facebook Pixel

2693. Call Function with Custom Context

Problem Description

This problem asks you to create a polyfill for JavaScript's built-in call() method. A polyfill is a piece of code that provides functionality that isn't natively supported.

The task is to add a method called callPolyfill to all JavaScript functions through the Function.prototype. This method should:

  1. Accept an object obj as its first parameter, which will become the this context for the function
  2. Accept any number of additional arguments that will be passed to the original function
  3. Execute the function with the specified this context and arguments

The key challenge is that you cannot use the built-in Function.call() method to implement this.

How it works:

When a function is called normally in JavaScript, its this context might be undefined or refer to the global object. The callPolyfill method allows you to explicitly set what this refers to inside the function.

Example:

Given a function:

function tax(price, taxRate) {
  const totalCost = price * (1 + taxRate);
  console.log(`The cost of ${this.item} is ${totalCost}`);
}
  • Calling tax(10, 0.1) normally would log "The cost of undefined is 11" because this.item is undefined
  • Calling tax.callPolyfill({item: "salad"}, 10, 0.1) would log "The cost of salad is 11" because this is set to {item: "salad"}

The solution uses the bind() method to create a new function with the specified this context, then immediately invokes it with the provided arguments. The bind() method returns a new function with this permanently bound to the specified context, which is then called with the spread operator ...args to pass all arguments.

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

Intuition

The core challenge is to change the this context of a function without using the built-in call() method. We need to find another way to bind a specific object as the this context when executing a function.

Let's think about what JavaScript methods we have available for manipulating this:

  1. call() - explicitly forbidden in this problem
  2. apply() - likely also off-limits since it's similar to call()
  3. bind() - creates a new function with a fixed this context

The bind() method stands out as a viable option. When you use bind(), it doesn't immediately execute the function. Instead, it returns a new function where this is permanently set to whatever you passed as the first argument.

Here's the key insight: If we can create a new function with the correct this context using bind(), we can then immediately invoke that new function with the provided arguments.

The solution becomes straightforward:

  1. Use this.bind(context) to create a new function where this inside the function will refer to context
  2. Immediately invoke this new bound function with the arguments using the spread operator (...args)

This two-step process effectively mimics what call() does in a single step - it sets the this context and executes the function with arguments.

The elegance of this approach is that bind() handles all the complex internal mechanics of setting up the proper this binding, and we just need to execute the resulting function. It's like creating a temporary function that "remembers" what its this should be, then running it right away.

Solution Approach

The implementation extends the Function.prototype to add the callPolyfill method to all functions in JavaScript. Here's how the solution works step by step:

1. Extending Function.prototype:

Function.prototype.callPolyfill = function (context, ...args): any {
    // implementation
};

By adding a method to Function.prototype, every function in JavaScript automatically gets access to this method. The this keyword inside this method refers to the function that callPolyfill is being called on.

2. Using the rest parameter for arguments:

function (context, ...args)

The first parameter context is the object that will become the this context. The ...args rest parameter collects all remaining arguments into an array, allowing the method to accept any number of additional arguments that need to be passed to the original function.

3. Creating a bound function:

const fn = this.bind(context);

Here, this refers to the original function (the one calling callPolyfill). The bind() method creates a new function where the this context is permanently set to the context object passed in. This bound function fn is like a copy of the original function but with a fixed this value.

4. Executing with arguments:

return fn(...args);

The bound function is immediately invoked with the spread operator ...args, which expands the array of arguments and passes them individually to the function. The result of the function execution is returned.

TypeScript Declaration:

declare global {
    interface Function {
        callPolyfill(context: Record<any, any>, ...args: any[]): any;
    }
}

This TypeScript declaration tells the type system that all functions now have a callPolyfill method, preventing type errors when using this custom method.

The beauty of this solution is its simplicity - it leverages the existing bind() method to handle the complex task of setting the this context, then immediately executes the resulting function. This achieves the same effect as call() without directly using it.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the callPolyfill solution works:

// Define a simple greeting function that uses 'this'
function greet(greeting, punctuation) {
    return `${greeting}, ${this.name}${punctuation}`;
}

// Create an object to use as context
const person = { name: "Alice" };

// Using our callPolyfill implementation
const result = greet.callPolyfill(person, "Hello", "!");

Step-by-step execution:

  1. Method invocation: When greet.callPolyfill(person, "Hello", "!") is called:

    • this inside callPolyfill refers to the greet function
    • context = person object ({ name: "Alice" })
    • args = ["Hello", "!"] (collected by rest parameter)
  2. Creating bound function:

    const fn = this.bind(context);
    // fn is now a new function where 'this' is locked to the person object

    The bind() call creates a new function that's identical to greet, but whenever it runs, its this will always refer to the person object.

  3. Executing with arguments:

    return fn(...args);
    // Equivalent to: fn("Hello", "!")

    The spread operator expands the array ["Hello", "!"] into individual arguments. The bound function executes with:

    • this.nameperson.name"Alice"
    • greeting"Hello"
    • punctuation"!"
  4. Final result: The function returns "Hello, Alice!", successfully using the person object as the this context.

Without callPolyfill, calling greet("Hello", "!") directly would fail because this.name would be undefined (in strict mode) or refer to the global object. The polyfill allows us to explicitly control what this refers to, just like the native call() method does.

Solution Implementation

1from typing import Any, Callable, Dict, List
2
3class FunctionPolyfill:
4    """
5    A class to demonstrate polyfill implementation for Function.prototype.call
6    Since Python doesn't have prototype-based inheritance like JavaScript,
7    we'll create a wrapper class to simulate similar behavior
8    """
9  
10    @staticmethod
11    def call_polyfill(func: Callable, context: Dict[str, Any], *args: Any) -> Any:
12        """
13        Polyfill implementation for Function.prototype.call
14        This method allows a function to be called with a given 'this' value and arguments
15      
16        Args:
17            func: The function to be executed
18            context: The object that will be bound as 'this' inside the function
19            *args: Variable arguments that will be passed to the function
20          
21        Returns:
22            The return value of the function after execution
23        """
24        # In Python, we simulate binding by creating a wrapper function
25        # that has access to the context as 'self'
26        class ContextWrapper:
27            def __init__(self, ctx: Dict[str, Any]):
28                # Copy context attributes to this wrapper instance
29                for key, value in ctx.items():
30                    setattr(self, key, value)
31          
32            def execute(self, *arguments: Any) -> Any:
33                # Execute the function with this wrapper as 'self'
34                return func(self, *arguments)
35      
36        # Create an instance with the provided context
37        wrapper = ContextWrapper(context)
38      
39        # Execute the bound function with the provided arguments
40        return wrapper.execute(*args)
41
42
43# Alternative implementation using a decorator approach
44def add_call_polyfill(func: Callable) -> Callable:
45    """
46    Decorator to add call_polyfill method to a function
47  
48    Args:
49        func: The function to be decorated
50      
51    Returns:
52        The decorated function with call_polyfill method
53    """
54    def call_polyfill(context: Dict[str, Any], *args: Any) -> Any:
55        """
56        Custom implementation of the call method
57      
58        Args:
59            context: The object to be used as 'this' when executing the function
60            *args: Arguments to pass to the function
61          
62        Returns:
63            The result of the function execution
64        """
65        # Create a namespace object with context attributes
66        namespace = type('Namespace', (), context)()
67      
68        # Bind the function to the namespace and execute
69        bound_method = func.__get__(namespace, type(namespace))
70        return bound_method(*args)
71  
72    # Attach the call_polyfill method to the function
73    func.call_polyfill = call_polyfill
74    return func
75
76
77# Example usage demonstration
78@add_call_polyfill
79def increment(self) -> int:
80    """
81    Example function that increments a count attribute
82  
83    Returns:
84        The incremented count value
85    """
86    self.count += 1
87    return self.count
88
89
90# Example usage:
91# result = increment.call_polyfill({'count': 1})  # Returns 2
92
1/**
2 * Interface representing a JavaScript-like function that can be called with custom context
3 */
4interface CallableFunction {
5    /**
6     * Custom implementation of the call method
7     * @param context - The object to be used as 'this' when executing the function
8     * @param args - Arguments to pass to the function
9     * @return The result of the function execution
10     */
11    Object callPolyfill(Object context, Object... args);
12}
13
14/**
15 * Abstract base class that implements the callPolyfill functionality
16 * This simulates JavaScript's Function.prototype behavior in Java
17 */
18abstract class Function implements CallableFunction {
19  
20    /**
21     * Abstract method that derived classes must implement to define the function logic
22     * @param context - The context object that acts as 'this'
23     * @param args - Arguments passed to the function
24     * @return The result of the function execution
25     */
26    protected abstract Object execute(Object context, Object... args);
27  
28    /**
29     * Polyfill implementation for Function.prototype.call
30     * This method allows a function to be called with a given 'this' value and arguments
31     * 
32     * @param context - The object that will be bound as 'this' inside the function
33     * @param args - Varargs parameters that will be passed as arguments to the function
34     * @return The return value of the function after execution
35     */
36    @Override
37    public Object callPolyfill(Object context, Object... args) {
38        // Execute the function with the provided context and arguments
39        // In Java, we directly call execute since we can't dynamically bind like JavaScript
40        return execute(context, args);
41    }
42}
43
44/**
45 * Example implementation class demonstrating usage
46 */
47class IncrementFunction extends Function {
48    @Override
49    protected Object execute(Object context, Object... args) {
50        // Cast context to a Map to access properties
51        if (context instanceof java.util.Map) {
52            @SuppressWarnings("unchecked")
53            java.util.Map<String, Object> contextMap = (java.util.Map<String, Object>) context;
54          
55            // Get current count value
56            Integer count = (Integer) contextMap.get("count");
57            if (count != null) {
58                // Increment the count
59                count++;
60                // Update the context with new count value
61                contextMap.put("count", count);
62                // Return the new count
63                return count;
64            }
65        }
66        return null;
67    }
68}
69
70/**
71 * Example usage:
72 * Map<String, Object> context = new HashMap<>();
73 * context.put("count", 1);
74 * Function increment = new IncrementFunction();
75 * increment.callPolyfill(context); // Returns 2
76 */
77
1#include <iostream>
2#include <functional>
3#include <any>
4#include <vector>
5#include <unordered_map>
6
7/**
8 * Context class to simulate JavaScript object behavior
9 * Acts as a container for key-value pairs that can be used as 'this' context
10 */
11class Context {
12public:
13    std::unordered_map<std::string, std::any> properties;
14  
15    /**
16     * Get property value by key
17     * @param key - The property name
18     * @return Reference to the property value
19     */
20    std::any& operator[](const std::string& key) {
21        return properties[key];
22    }
23};
24
25/**
26 * Base class for functions that support callPolyfill method
27 * Template parameter R is return type, Args are function argument types
28 */
29template<typename R, typename... Args>
30class CallableFunction {
31private:
32    std::function<R(Context*, Args...)> m_function;
33  
34public:
35    /**
36     * Constructor that takes a function with Context pointer as first parameter
37     * @param func - The function to wrap
38     */
39    CallableFunction(std::function<R(Context*, Args...)> func) : m_function(func) {}
40  
41    /**
42     * Polyfill implementation for Function.prototype.call
43     * This method allows a function to be called with a given 'this' value and arguments
44     * 
45     * @param context - The object that will be bound as 'this' inside the function
46     * @param args - Arguments that will be passed to the function
47     * @return The return value of the function after execution
48     */
49    R callPolyfill(Context* context, Args... args) {
50        // Execute the function with the provided context and arguments
51        // The context acts as 'this' binding in JavaScript
52        return m_function(context, args...);
53    }
54  
55    /**
56     * Regular function call operator for direct invocation
57     * @param context - The context object
58     * @param args - Function arguments
59     * @return The return value of the function
60     */
61    R operator()(Context* context, Args... args) {
62        return m_function(context, args...);
63    }
64};
65
66/**
67 * Example usage demonstration:
68 * 
69 * // Define a function that increments count property
70 * auto increment = CallableFunction<int>([](Context* context) {
71 *     int& count = std::any_cast<int&>((*context)["count"]);
72 *     count++;
73 *     return count;
74 * });
75 * 
76 * // Create context object with count property
77 * Context ctx;
78 * ctx["count"] = 1;
79 * 
80 * // Call function with context binding
81 * int result = increment.callPolyfill(&ctx); // Returns 2
82 */
83
84// Example implementation
85int main() {
86    // Create an increment function that uses 'this' context
87    auto increment = CallableFunction<int>([](Context* context) {
88        // Access and modify the count property from context
89        int& count = std::any_cast<int&>((*context)["count"]);
90        count++;
91        return count;
92    });
93  
94    // Create context object with initial count value
95    Context context;
96    context["count"] = 1;
97  
98    // Call the function using callPolyfill with the context
99    int result = increment.callPolyfill(&context);
100    std::cout << "Result: " << result << std::endl; // Output: 2
101  
102    return 0;
103}
104
1// Extend the global Function interface to include the callPolyfill method
2declare global {
3    interface Function {
4        /**
5         * Custom implementation of the call method
6         * @param context - The object to be used as 'this' when executing the function
7         * @param args - Arguments to pass to the function
8         * @returns The result of the function execution
9         */
10        callPolyfill(context: Record<any, any>, ...args: any[]): any;
11    }
12}
13
14/**
15 * Polyfill implementation for Function.prototype.call
16 * This method allows a function to be called with a given 'this' value and arguments
17 * 
18 * @param context - The object that will be bound as 'this' inside the function
19 * @param args - Rest parameters that will be passed as arguments to the function
20 * @returns The return value of the function after execution
21 */
22Function.prototype.callPolyfill = function (context: Record<any, any>, ...args: any[]): any {
23    // Bind the function to the provided context
24    const boundFunction = this.bind(context);
25  
26    // Execute the bound function with the provided arguments
27    return boundFunction(...args);
28};
29
30/**
31 * Example usage:
32 * function increment() { this.count++; return this.count; }
33 * increment.callPolyfill({count: 1}); // 2
34 */
35

Time and Space Complexity

Time Complexity: O(1)

The callPolyfill function performs a constant number of operations:

  • this.bind(context) creates a new bound function - O(1)
  • fn(...args) invokes the bound function with spread arguments - O(1) for the invocation itself (excluding the execution time of the actual function being called)

The time complexity is constant because regardless of the number of arguments passed, the binding and invocation operations themselves take constant time. The actual execution time of the function being called is not included in this analysis.

Space Complexity: O(n) where n is the number of arguments

The space complexity consists of:

  • The bound function created by bind() - O(1) for the function wrapper
  • The args array from rest parameters - O(n) where n is the number of arguments
  • The spread operation ...args when calling fn - O(n) for unpacking the arguments

The dominant factor is the storage of arguments, making the overall space complexity linear with respect to the number of arguments passed to the function.

Common Pitfalls

1. Loss of Original Function Properties and Methods

When using bind() to create a new function, the bound function doesn't inherit custom properties that may have been added to the original function. This can break code that relies on function metadata or attached properties.

Problem Example:

function myFunc() {
    return this.value;
}
myFunc.customProperty = "important data";
myFunc.helperMethod = function() { return "helper"; };

// After binding, these properties are lost
const boundFunc = myFunc.bind({value: 42});
console.log(boundFunc.customProperty); // undefined
console.log(boundFunc.helperMethod);   // undefined

Solution: Copy properties from the original function to maintain compatibility:

Function.prototype.callPolyfill = function(context, ...args) {
    const fn = this.bind(context);
    // Copy properties from original function
    Object.setPrototypeOf(fn, this);
    return fn(...args);
};

2. Incorrect Handling of null or undefined Context

In JavaScript's native call(), passing null or undefined as context in non-strict mode defaults to the global object, while in strict mode it remains null or undefined. The simple bind() approach doesn't handle this nuance correctly.

Problem Example:

function getValue() {
    return this;
}

// Native call behavior
console.log(getValue.call(null));      // null in strict mode, global object in non-strict
console.log(getValue.call(undefined)); // undefined in strict mode, global object in non-strict

// Polyfill might not match this behavior

Solution: Check for null or undefined and handle appropriately:

Function.prototype.callPolyfill = function(context, ...args) {
    // Handle null/undefined context based on strict mode
    if (context === null || context === undefined) {
        context = (function() { return this; })() || context;
    }
    const fn = this.bind(context);
    return fn(...args);
};

3. Performance Overhead from Creating Intermediate Functions

Creating a new bound function via bind() just to immediately invoke it adds unnecessary overhead. For frequently called functions, this can impact performance.

Problem Example:

// This creates a new function object every time
for (let i = 0; i < 1000000; i++) {
    someFunc.callPolyfill(obj, arg1, arg2); // Creates intermediate bound function
}

Solution: Use a more direct approach by temporarily attaching the function to the context object:

Function.prototype.callPolyfill = function(context, ...args) {
    if (context === null || context === undefined) {
        return this(...args);
    }
  
    // Use a unique symbol to avoid property conflicts
    const fnSymbol = Symbol('fn');
    context[fnSymbol] = this;
    const result = context[fnSymbol](...args);
    delete context[fnSymbol];
    return result;
};

4. Primitive Values as Context

When primitive values (strings, numbers, booleans) are passed as context, they need to be boxed into their object wrappers, which the simple bind() approach handles but may not be obvious.

Problem Example:

function getType() {
    return typeof this;
}

// With native call
console.log(getType.call(5));        // "object" (Number object)
console.log(getType.call("hello"));  // "object" (String object)
console.log(getType.call(true));     // "object" (Boolean object)

Solution: Explicitly handle primitive boxing if needed for clarity:

Function.prototype.callPolyfill = function(context, ...args) {
    // Box primitives explicitly
    if (context !== null && context !== undefined) {
        const type = typeof context;
        if (type === 'string' || type === 'number' || type === 'boolean') {
            context = Object(context);
        }
    }
    const fn = this.bind(context);
    return fn(...args);
};

5. Arrow Functions Cannot Be Bound

Arrow functions have lexically bound this that cannot be changed. The polyfill will fail silently or produce unexpected results with arrow functions.

Problem Example:

const arrowFunc = () => {
    return this.value;
};

// This won't work as expected
arrowFunc.callPolyfill({value: 42}); // Returns undefined or outer scope's this.value

Solution: Add a check for arrow functions and handle appropriately:

Function.prototype.callPolyfill = function(context, ...args) {
    // Check if it's an arrow function (no prototype property)
    if (!this.prototype) {
        console.warn('Arrow functions cannot have their context rebound');
        return this(...args);
    }
    const fn = this.bind(context);
    return fn(...args);
};
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More