Call, Bind and Apply in Javascript
If you write JavaScript, you've fought this. The confusion happens because we bring mental models from languages like Java or C++, where this is tied to the class it was written in. JavaScript doesn’t care where you wrote the function. It only cares about who called the function at runtime. A mental model I can think of is, look at the left of the dot. Lets look at an example.
const service = {
name: "Payment Gateway",
start: function() {
console.log(`Starting ${this.name}`);
}
};
service.start();
Look at service.start(). Who is on the left of the dot? service. So, this = service.
const startService = service.start;
startService();
Look at startService(). There is nothing to the left of the dot. It’s just a raw function call. The context is lost, this defaults to undefined, and your app crashes. To fix this, JavaScript gives us three methods on the Function.prototype to manually force the this context call, apply, and bind.
1. CALL
call immediately fires the function, forcing this to be the first argument you pass. Any remaining arguments are passed individually.
function logError(level, message) {
console.log(`[\({this.service}] \){level}: ${message}`);
}
const context = { service: "Auth" };
logError.call(context, "CRITICAL", "connection lost");
2. APPLY
apply is exactly the same as call, but it takes the function's arguments as a single array.
logError.apply(context, ["CRITICAL", "connection lost"]);
3. BIND
bind doesn't execute the function immediately. Instead, it returns a new function with this permanently locked to the object you provided. This is how you fix the callback problem.
const safeStart = service.start.bind(service);
setTimeout(safeStart, 1000);
writing the polyfills
To truly understand a system, you build it yourself. If we were to write these methods from scratch, how would the engine actually do it?
The call Polyfill
trick is simple, temporarily attach the function to the target object as a property. Then call it. By doing this, the object is literally to the left of the dot.
Function.prototype.myCall = function(context = globalThis, ...args) {
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...args);
delete context[fnKey];
return result;
};
The apply Polyfill
Almost identical to call, but we spread an array.
Function.prototype.myApply = function(context = globalThis, argsArray = []) {
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...argsArray);
delete context[fnKey];
return result;
};
The bind Polyfill
bind needs to return a closure. When that closure is eventually called, it uses apply internally to execute the original function with the locked context.
Function.prototype.myBind = function(context = globalThis, ...boundArgs) {
const originalFn = this;
return function(...executionArgs) {
return originalFn.apply(context, [...boundArgs, ...executionArgs]);
};
};
Mastering execution context moves you from randomly guessing why a callback broke, to deliberately architecting how your data flows through asynchronous boundaries.