Usage and Custom Implementation of JavaScript's apply, call, and bind Methods
Core Differences and Basic Usage
const userCtx = { username: "luna" };
function displayProfile(age, gender) {
console.log(`Username: ${this.username}, Age: ${age}, Gender: ${gender}`);
}
// apply accepts target context and array of parameters, executes immediately
displayProfile.apply(userCtx, [24, "female"]);
// call accepts target context and discrete parameters, executes immediately
displayProfile.call(userCtx, 24, "female");
// bind returns a new function with bound context and pre-filled parameters, requires explicit execution
displayProfile.bind(userCtx, 24, "female")();
Custom apply Implementation
The core challenge of implementing apply is expanding the input parameter array into discrete arguments for functoin invocation, which can be handled via eval for broad compatibility.
Function.prototype.customApply = function (targetCtx, paramList) {
// Fallback to global window object if no valid context is provided
const executionCtx = targetCtx || window;
// Attach current function as a temporary method to target context to bind this pointer
executionCtx.tempFn = this;
// Handle empty parameter list edge case
if (!paramList || paramList.length === 0) {
const result = executionCtx.tempFn();
delete executionCtx.tempFn;
return result;
}
// Build argument string for eval execution
const argStr = paramList.map((_, idx) => `paramList[${idx}]`).join(",");
const runResult = eval(`executionCtx.tempFn(${argStr})`);
// Clean up temporary property to avoid side effects
delete executionCtx.tempFn;
return runResult;
};
Test verification:
const calcCtx = { base: 10 };
function computeSum(addVal1, addVal2) {
console.log(this.base + addVal1 + addVal2);
}
computeSum.customApply(calcCtx, [5, 3]);
Output:
18
Custom call Implementation
call shares almost identical logic with apply, the only difference being that it accepts discrete parameters instead of a single parameter array.
Function.prototype.customCall = function (targetCtx) {
const executionCtx = targetCtx || window;
executionCtx.tempFn = this;
// Extract all parameters passed after the context argument
const params = Array.prototype.slice.call(arguments, 1);
if (params.length === 0) {
const result = executionCtx.tempFn();
delete executionCtx.tempFn;
return result;
}
const argStr = params.map((_, idx) => `params[${idx}]`).join(",");
const runResult = eval(`executionCtx.tempFn(${argStr})`);
delete executionCtx.tempFn;
return runResult;
};
Test verification:
const calcCtx = { base: 10 };
function computeSum(addVal1, addVal2) {
console.log(this.base + addVal1 + addVal2);
}
computeSum.customCall(calcCtx, 5, 3);
Output:
18
Custom bind Implementation
bind has 3 key behaviors that need too be replicated:
- Returns a new function with pre-bound context
- Supports partial parameter application (curying)
- Ignores pre-bound context when the returned function is used as a constructer, and preserves the original function's prototype chain for generated instances
Function.prototype.customBind = function (targetCtx) {
const executionCtx = targetCtx || window;
const originalFn = this;
// Extract pre-filled parameters passed during bind invocation
const prefilledParams = Array.prototype.slice.call(arguments, 1);
function BoundFn() {
// Combine pre-filled parameters with parameters passed during actual invocation
const fullParams = prefilledParams.concat(Array.prototype.slice.call(arguments));
// Use new instance as context if called as constructor, otherwise use pre-bound context
const runCtx = this instanceof originalFn ? this : executionCtx;
return originalFn.customApply(runCtx, fullParams);
}
// Bridge prototype chain to preserve inheritance for instances generated via new
function ProtoBridge() {}
ProtoBridge.prototype = originalFn.prototype;
BoundFn.prototype = new ProtoBridge();
return BoundFn;
};
Test verification:
const testCtx = { val: 2 };
function TestConstructor(p1, p2) {
console.log(this.val, p1, p2);
}
TestConstructor.prototype.sharedProp = 100;
const BoundConstructor = TestConstructor.customBind(testCtx, 5);
// Normal function invocation
BoundConstructor(7);
// Invocation as constructor
const instance = new BoundConstructor(7);
// Prototype inheritance check
console.log(instance.sharedProp);
Output:
2 5 7
undefined 5 7
100