JavaScript 'this' Explained: call/apply/bind + Arrow Functions
A practical guide to 'this' in JavaScript: default binding, implicit binding, explicit binding (call/apply/bind), new binding, and how arrow functions change it.
On this page
30‑Second Senior Interview Answer
In JavaScript, this is not lexical in regular functions; it is determined strictly at runtime by how and where the function is called (its call-site).
The value of this is governed by a strict 4-tier precedence hierarchy:
newBinding (highest priority):thispoints to the freshly instantiated object.- Explicit & Hard Binding (
call,apply,bind):thisis explicitly specified by the caller. - Implicit Binding:
thisis the object context immediately preceding the dot at the call-site (obj.method()). - Default Binding (lowest priority):
thisresolves toundefinedin strict mode ("use strict"), orglobalThis(windowin browsers) in non-strict mode.
Arrow functions are the sole exception: they do not define their own this binding. Instead, they capture this lexically from their enclosing lexical context at the time of creation, and their this value cannot be altered even by call, apply, or bind.
1. The Precedence Hierarchy (Memorize This Order)
When an interviewer shows a tricky code snippet, determine the value of this by asking these questions in strict order:
flowchart TD
Q1{"Called with new?"}
Q1 -- Yes --> A1["1. new Binding<br/>(Newly created object)"]
Q1 -- No --> Q2{"Called with call/apply/bind?"}
Q2 -- Yes --> A2["2. Explicit Binding<br/>(Specified context object)"]
Q2 -- No --> Q3{"Called on a context object?<br/>(obj.fn())"}
Q3 -- Yes --> A3["3. Implicit Binding<br/>(Context object preceding dot)"]
Q3 -- No --> A4["4. Default Binding<br/>(undefined in strict, global in non-strict)"]2. Deep Dive: The 4 Rules
Rule 1: Default Binding (Standalone Function Invocation)
When a function is called without any qualifying context:
function show() {
console.log(this);
}
show(); // Window (non-strict) OR undefined (strict mode)In ES modules and React components, strict mode is enforced automatically, meaning default binding always evaluates to undefined. Attempting to access properties like this.name will throw TypeError: Cannot read properties of undefined.
Rule 2: Implicit Binding (Object Property Call-Site)
When a function is invoked as a method of an object, implicit binding sets this to the object directly before the call dot:
const user = {
name: 'Rajesh',
greet() {
console.log(`Hello, ${this.name}`);
},
};
user.greet(); // "Hello, Rajesh" (call-site: user.greet)The "Losing the Call-Site" Trap (Favorite Interview Question)
Implicit binding is fragile. Extracting a method reference or passing it as a callback immediately strips its object context:
const user = {
name: 'Rajesh',
greet() {
console.log(this.name);
},
};
// Trap 1: Variable assignment
const standaloneGreet = user.greet;
standaloneGreet(); // undefined (or TypeError in strict mode)
// Trap 2: Passed as callback to setTimeout or event listener
setTimeout(user.greet, 100); // undefined (callback executed via default binding)Rule 3: Explicit Binding (call, apply, bind)
To forcefully set this regardless of the call-site, use Function.prototype methods:
function introduce(greeting, punctuation) {
console.log(`${greeting}, I am ${this.name}${punctuation}`);
}
const developer = { name: 'Rajesh' };
// .call(thisArg, arg1, arg2, ...) -> Invokes immediately
introduce.call(developer, 'Hi', '!');
// .apply(thisArg, [argsArray]) -> Invokes immediately
introduce.apply(developer, ['Hey', '.']);
// .bind(thisArg, arg1, ...) -> Returns a new permanently bound function
const boundIntro = introduce.bind(developer, 'Greetings');
boundIntro('?');Rule 4: new Binding
When a function is called with the new operator (as a constructor), JavaScript executes a precise 4-step algorithm:
- A brand new plain object (
{}) is created in memory. - The new object's internal prototype (
[[Prototype]]) is linked to the constructor function's.prototype. - The constructor function is executed with
thisbound to the newly created object. - If the constructor explicitly returns a non-primitive object, that object is returned. Otherwise, the object created in Step 1 is returned.
function Engineer(name, role) {
// 1. this = Object.create(Engineer.prototype)
this.name = name;
this.role = role;
// 4. return this (implicit)
}
const eng = new Engineer('Rajesh', 'Staff Engineer');
console.log(eng.name); // "Rajesh"[!WARNING] Constructor Return Override Trap: If a constructor explicitly returns an object (
return { custom: true }), the newly createdthisinstance is discarded. However, if it returns a primitive (return 42orreturn "text"), the return value is ignored andthisis returned.
3. Whiteboard Polyfill: Function.prototype.bind
Interviewers frequently ask candidates to write a production-grade polyfill for bind. A naive polyfill is trivial, but a senior candidate must handle currying (partial application) and the new operator override.
Function.prototype.myBind = function (context, ...boundArgs) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
const targetFn = this;
function boundFunction(...callArgs) {
// If called with `new boundFunction()`, `this` is an instance of boundFunction.
// In that case, use the newly created `this` instead of the original `context`!
const isNew = this instanceof boundFunction;
const finalContext = isNew ? this : context;
return targetFn.apply(finalContext, [...boundArgs, ...callArgs]);
}
// Preserve prototype chain for instances created via `new`
if (this.prototype) {
boundFunction.prototype = Object.create(this.prototype);
}
return boundFunction;
};Whiteboard Polyfill: Function.prototype.call
Using modern JavaScript symbols to avoid property collisions on the context object:
Function.prototype.myCall = function (context, ...args) {
// If context is null or undefined, default to globalThis
const targetContext = context ?? globalThis;
// Use Symbol to avoid overwriting existing properties
const fnKey = Symbol('fn');
// Box primitives into objects if needed (e.g. Object(targetContext))
const boxedContext = Object(targetContext);
boxedContext[fnKey] = this;
try {
return boxedContext[fnKey](...args);
} finally {
delete boxedContext[fnKey];
}
};4. Arrow Functions: The Lexical Exception
Arrow functions do not have their own this. They do not possess an internal [[Construct]] method (cannot be used with new) and do not create an execution context this binding.
const company = {
name: 'Eunix Tech',
employees: ['Rajesh', 'Amit'],
printRoster() {
// Regular function here: `this` === company
this.employees.forEach((emp) => {
// Arrow function captures `this` from printRoster lexical scope:
console.log(`${emp} works at ${this.name}`);
});
},
};
company.printRoster();
// "Rajesh works at Eunix Tech"
// "Amit works at Eunix Tech"Arrow Functions Ignore Explicit Binding
Because arrow functions do not have a this binding, call, apply, and bind cannot alter their this:
const getArrowThis = () => this;
const customContext = { name: 'Eunix' };
console.log(getArrowThis.call(customContext) === customContext); // ❌ FALSE5. Senior Interview Edge Cases & Puzzles
Puzzle 1: Object Literal is NOT a Scope!
const profile = {
name: 'Rajesh',
getName: () => {
return this.name;
},
};
console.log(profile.getName());Answer: undefined.
Why? Curly braces { ... } in an object literal create an object, not a block or functional scope. The arrow function captures this from the enclosing scope where profile was defined (the module or window), where this.name is undefined.
Puzzle 2: new vs .bind() — Who Wins?
function Point(x, y) {
this.x = x;
this.y = y;
}
const fakeContext = { x: 99, y: 99 };
const BoundPoint = Point.bind(fakeContext);
const pt = new BoundPoint(1, 2);
console.log(pt.x, pt.y);
console.log(fakeContext.x, fakeContext.y);Answer:
pt.x = 1, pt.y = 2
fakeContext.x = 99, fakeContext.y = 99
Why? According to the specification, the new operator overrides hard binding. When BoundPoint is instantiated with new, the bound context fakeContext is ignored, and a new instance of Point is created.
Puzzle 3: Nested Method Execution Trace
What does this print?
const calculator = {
multiplier: 2,
compute(arr) {
return arr.map(function (n) {
return n * this.multiplier;
});
},
};
console.log(calculator.compute([1, 2, 3]));Answer: [NaN, NaN, NaN] (or TypeError in strict mode).
Why? The anonymous function passed inside map is invoked as a standard callback (default binding). Inside that callback, this is window (or undefined), meaning this.multiplier is undefined. 1 * undefined === NaN.
How to fix it:
- Pass arrow function:
arr.map(n => n * this.multiplier) - Pass
thisArgtomap:arr.map(function(n) { ... }, this) - Pre-bind:
arr.map(fn.bind(this))
Summary Checklist
- I know the 4 rules in order:
new> Explicit > Implicit > Default. - In strict mode, default binding is
undefined, notwindow. - Detaching a method (
const f = obj.method) drops implicit binding. - Arrow functions capture
thislexically and ignorecall/apply/bind. - Object literal braces
{}do NOT create a lexicalthisscope. - I can write the whiteboard polyfill for
bindincluding partial application andnewhandling.
Written by Rajesh Dhiman at Eunix Tech.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
Written by Rajesh Dhiman
Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.