Why Is this So Confusing?
In most languages, this always refers to the instance of the class where the method is defined. In JavaScript, this is determined by how a function is called, not where it is written. The same function can have different this values depending on the call site.
function greet() {
console.log(`Hello, ${this.name}`);
}
const alice = { name: 'Alice', greet };
const bob = { name: 'Bob', greet };
alice.greet(); // Hello, Alice
bob.greet(); // Hello, Bob
greet(); // Hello, undefined (or error in strict mode)
Same function, three different this values. Once you understand the rules, the confusion disappears entirely.
The Four Binding Rules
JavaScript determines this using four rules, applied in this priority order:
Priority Rule When It Applies
────────────────────────────────────────────────────────────
1 new binding Function called with new keyword
2 Explicit binding call(), apply(), or bind()
3 Implicit binding Function called as object method
4 Default binding Standalone function call
Let's examine each one in detail.
Rule 1: Default Binding
When a function is called as a standalone function (not as a method, not with new, not with call/apply/bind), this falls back to the default.
In Non-Strict Mode
function showThis() {
console.log(this);
}
showThis(); // Window object (browser) or global object (Node.js)
In Strict Mode
'use strict';
function showThis() {
console.log(this);
}
showThis(); // undefined
globalThis (ES2020)
The globalThis property provides a standard way to access the global this value across environments.
// Works everywhere: browser, Node.js, Web Workers
console.log(globalThis);
// Browser: globalThis === window
// Node.js: globalThis === global
// Worker: globalThis === self
Node.js Module Context
In Node.js, the top-level this in a module is not the global object — it is module.exports.
// In a Node.js file (CommonJS)
console.log(this === module.exports); // true
console.log(this === global); // false
// But inside a function
function test() {
console.log(this === global); // true (non-strict)
}
test();
Rule 2: Implicit Binding
When a function is called as a method of an object (using dot notation), this refers to the object to the left of the dot.
const user = {
name: 'Alice',
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
user.greet(); // Hi, I'm Alice
Nested Objects
Only the immediate (last) object in the chain matters.
const company = {
name: 'Acme Corp',
department: {
name: 'Engineering',
getName() {
return this.name;
}
}
};
console.log(company.department.getName()); // 'Engineering' (not 'Acme Corp')
Implicit Binding Loss
This is the most common source of this bugs. When you extract a method from an object, the binding is lost.
const user = {
name: 'Alice',
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
// Works fine — implicit binding
user.greet(); // Hi, I'm Alice
// Broken — method reference loses binding
const greetFn = user.greet;
greetFn(); // Hi, I'm undefined
// Also broken — passing as callback
setTimeout(user.greet, 100); // Hi, I'm undefined
Here is what happens under the hood:
user.greet() greetFn()
| |
v v
this = user this = global/undefined
| |
v v
"Hi, I'm Alice" "Hi, I'm undefined"
When you assign user.greet to greetFn, you get a plain function reference. The connection to user is gone. The function no longer "knows" it came from user.
Rule 3: Explicit Binding — call, apply, bind
You can force this to any value using call(), apply(), or bind().
call()
Calls the function immediately with a specified this and individual arguments.
function introduce(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
introduce.call(alice, 'Hello', '!'); // Hello, I'm Alice!
introduce.call(bob, 'Hey', '.'); // Hey, I'm Bob.
apply()
Same as call(), but takes arguments as an array.
introduce.apply(alice, ['Hello', '!']); // Hello, I'm Alice!
introduce.apply(bob, ['Hey', '.']); // Hey, I'm Bob.
call vs apply — When to Use Which
Method Arguments Use When
──────────────────────────────────────────────────
call() individual you know the args at write time
apply() array args are in an array already
// Practical: borrowing array methods
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
// Use call with Array.prototype methods
const arr = Array.prototype.slice.call(arrayLike);
console.log(arr); // ['a', 'b', 'c']
// apply is handy with Math.max
const numbers = [5, 2, 8, 1, 9];
Math.max.apply(null, numbers); // 9
// Modern alternative: spread
Math.max(...numbers); // 9
bind()
Returns a new function with this permanently set. Does not call the function immediately.
const user = {
name: 'Alice',
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
// Create a bound version
const boundGreet = user.greet.bind(user);
// Now it works everywhere
boundGreet(); // Hi, I'm Alice
setTimeout(boundGreet, 100); // Hi, I'm Alice
// bind can also pre-fill arguments (partial application)
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
console.log(double(10)); // 20
bind Is Permanent
Once bound, this cannot be overridden — not even by call or apply.
const user = { name: 'Alice' };
const other = { name: 'Bob' };
function greet() {
console.log(this.name);
}
const bound = greet.bind(user);
bound(); // Alice
bound.call(other); // Alice (still Alice, bind wins)
bound.apply(other); // Alice (still Alice)
Rule 4: new Binding
When a function is called with new, JavaScript creates a new object and sets this to that object.
function Person(name, age) {
// this = {} (new empty object, created automatically)
this.name = name;
this.age = age;
// return this (implicit)
}
const alice = new Person('Alice', 30);
console.log(alice.name); // Alice
console.log(alice.age); // 30
The new keyword does four things:
Step 1: Create a new empty object {}
Step 2: Link its [[Prototype]] to Person.prototype
Step 3: Set this = the new object this = {}
Step 4: Return the object (unless return this
function explicitly returns
a different object)
// Explicit return of object overrides new
function Weird() {
this.name = 'original';
return { name: 'override' }; // returning an object replaces this
}
const w = new Weird();
console.log(w.name); // 'override'
// But returning a primitive does NOT override
function Normal() {
this.name = 'original';
return 42; // primitives are ignored
}
const n = new Normal();
console.log(n.name); // 'original'
Arrow Functions — Lexical this
Arrow functions do not have their own this. They inherit this from the enclosing lexical scope — the scope where the arrow function was defined, not where it is called.
const user = {
name: 'Alice',
// Regular function — this = user (implicit binding)
greetRegular() {
console.log(`Regular: ${this.name}`);
},
// Arrow function — this = enclosing scope (NOT user)
greetArrow: () => {
console.log(`Arrow: ${this.name}`);
}
};
user.greetRegular(); // Regular: Alice
user.greetArrow(); // Arrow: undefined (this = global/module scope)
Where Arrow Functions Shine
Arrow functions solve the callback this problem permanently.
// Problem: regular function in callback loses this
const timer = {
seconds: 0,
start() {
setInterval(function () {
this.seconds++; // BUG: this = global, not timer
console.log(this.seconds);
}, 1000);
}
};
// Fix 1: Arrow function (best)
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // this = timer (lexical binding from start())
console.log(this.seconds);
}, 1000);
}
};
// Fix 2: const self = this (old pattern)
const timer = {
seconds: 0,
start() {
const self = this;
setInterval(function () {
self.seconds++;
console.log(self.seconds);
}, 1000);
}
};
// Fix 3: bind (also works)
const timer = {
seconds: 0,
start() {
setInterval(function () {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000);
}
};
Arrow Functions Cannot Be Rebound
call, apply, and bind have no effect on arrow functions' this.
const arrow = () => {
console.log(this);
};
const obj = { name: 'test' };
arrow.call(obj); // global/undefined (obj is ignored)
arrow.apply(obj); // global/undefined
arrow.bind(obj)(); // global/undefined
When NOT to Use Arrow Functions
// 1. Object methods — this will not refer to the object
const obj = {
value: 42,
getValue: () => this.value // undefined, not 42
};
// 2. Prototype methods
function Person(name) {
this.name = name;
}
Person.prototype.greet = () => {
return this.name; // undefined, not the instance
};
// 3. Event handlers that need the element
button.addEventListener('click', () => {
console.log(this); // Window, not the button
});
// Use regular function instead
button.addEventListener('click', function () {
console.log(this); // the button element
});
// 4. Constructors — arrow functions cannot be used with new
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor
this in Classes
ES6 classes use this the same way as constructor functions, but with cleaner syntax.
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
}
const alice = new User('Alice');
alice.greet(); // Hi, I'm Alice
// But the same extraction problem exists
const greetFn = alice.greet;
greetFn(); // TypeError in strict mode (class bodies are strict by default)
Class Fields Fix the Extraction Problem
class User {
constructor(name) {
this.name = name;
}
// Class field with arrow function — this is always bound to the instance
greet = () => {
console.log(`Hi, I'm ${this.name}`);
};
}
const alice = new User('Alice');
const greetFn = alice.greet;
greetFn(); // Hi, I'm Alice (works because arrow function captures this)
setTimeout(alice.greet, 100); // Hi, I'm Alice (works in callbacks too)
The tradeoff: each instance gets its own copy of the method (it is not on the prototype). For most apps this is fine, but it uses slightly more memory with thousands of instances.
this in Callbacks
Callbacks are the most common place where this goes wrong. Let's walk through every scenario.
Event Handlers
class App {
constructor() {
this.count = 0;
this.button = document.querySelector('#btn');
}
// Option 1: bind in constructor
init() {
this.button.addEventListener('click', this.handleClick.bind(this));
}
// Option 2: arrow function wrapper
init() {
this.button.addEventListener('click', (e) => this.handleClick(e));
}
// Option 3: class field arrow (defined above)
handleClick = () => {
this.count++;
console.log(`Clicked ${this.count} times`);
};
handleClick() {
this.count++;
console.log(`Clicked ${this.count} times`);
}
}
Array Methods
const processor = {
prefix: 'ITEM',
process(items) {
// Arrow function — this inherited from process()
return items.map(item => `${this.prefix}: ${item}`);
}
};
console.log(processor.process(['a', 'b', 'c']));
// ['ITEM: a', 'ITEM: b', 'ITEM: c']
// With regular function, this would be undefined/global
const broken = {
prefix: 'ITEM',
process(items) {
return items.map(function (item) {
return `${this.prefix}: ${item}`; // BUG: this is not broken
});
}
};
Promise Chains
class DataService {
constructor() {
this.baseUrl = 'https://api.example.com';
}
fetchUser(id) {
// Arrow functions keep this throughout the chain
return fetch(`${this.baseUrl}/users/${id}`)
.then(res => res.json())
.then(data => {
console.log(`Fetched from ${this.baseUrl}`); // works
return data;
});
}
}
The Complete Binding Decision Flowchart
When you see this in code, follow this decision tree:
Is it an arrow function?
├── YES → this = enclosing scope's this (lexical). Done.
└── NO → How is the function called?
│
├── new MyFunc()?
│ └── this = newly created object
│
├── func.call(obj) / func.apply(obj) / func.bind(obj)?
│ └── this = obj
│
├── obj.func()?
│ └── this = obj
│
└── func() (standalone)?
├── strict mode → this = undefined
└── sloppy mode → this = globalThis
Common Mistakes and Fixes
Mistake 1: Extracting Methods
// BUG
class Logger {
prefix = '[LOG]';
log(msg) {
console.log(`${this.prefix} ${msg}`);
}
}
const logger = new Logger();
const { log } = logger; // destructured — loses this
log('test'); // TypeError: Cannot read properties of undefined
// FIX: Use arrow function class field
class Logger {
prefix = '[LOG]';
log = (msg) => {
console.log(`${this.prefix} ${msg}`);
};
}
Mistake 2: Nested Functions
const counter = {
count: 0,
increment() {
// BUG: nested regular function
function addOne() {
this.count++; // this = global, not counter
}
addOne();
}
};
// FIX: arrow function
const counter = {
count: 0,
increment() {
const addOne = () => {
this.count++; // this = counter (lexical)
};
addOne();
}
};
Mistake 3: forEach with this
class TaskManager {
tasks = [];
addTasks(newTasks) {
// BUG: regular function callback
newTasks.forEach(function (task) {
this.tasks.push(task); // this = undefined in strict mode
});
// FIX 1: arrow function
newTasks.forEach((task) => {
this.tasks.push(task); // works
});
// FIX 2: thisArg parameter (forEach, map, filter, etc.)
newTasks.forEach(function (task) {
this.tasks.push(task); // works
}, this);
}
}
Mistake 4: setTimeout and setInterval
class Slideshow {
currentSlide = 0;
start() {
// BUG
setInterval(function () {
this.currentSlide++; // this = global
}, 3000);
// FIX
setInterval(() => {
this.currentSlide++; // this = Slideshow instance
}, 3000);
}
}
Determining this at a Glance — Quick Reference
Call Style this Value Example
─────────────────────────────────────────────────────────────────────────
func() global / undefined greet()
obj.func() obj user.greet()
func.call(obj) obj greet.call(user)
func.apply(obj) obj greet.apply(user)
func.bind(obj)() obj greet.bind(user)()
new Func() new instance new User()
() => {} enclosing this setTimeout(() => {})
class method instance (if called right) user.greet()
event handler (regular) DOM element btn.onclick = fn
event handler (arrow) enclosing this btn.onclick = () => {}
Key Takeaways
thisis determined at call time, not definition time (except for arrow functions)- Four rules in priority order:
new> explicit (call/apply/bind) > implicit (dot notation) > default (standalone) - Arrow functions have lexical this — they inherit from the enclosing scope and cannot be rebound
bind()returns a new permanently-bound function;call()andapply()invoke immediately- The most common bug is implicit binding loss — extracting a method from an object strips its context
- Use arrow function class fields to avoid binding issues in modern code
globalThisprovides a cross-environment way to access the global object- When in doubt, follow the decision flowchart: arrow? -> new? -> explicit? -> implicit? -> default