this is one of the most confusing concepts in JavaScript. You've probably seen code like this break unexpectedly:
const user = {
name: 'Alice',
greet() {
console.log('Hello, ' + this.name);
}
};
user.greet(); // "Hello, Alice" ✓
const greet = user.greet;
greet(); // "Hello, undefined" ✗
Same function, different results. Why?
The answer: this is not determined by where a function is defined, but by how it's called. Let's understand what that really means and how JavaScript determines this at the engine level.
What is 'this'? A Binding, Not a Variable
First, let's clear up a misconception: this is not a variable. You can't reassign it like this = something. It's a special binding that gets set when a function is invoked.
When JavaScript creates an Execution Context (which happens every time you call a function), it sets up:
- Variable Environment (local variables)
- Lexical Environment (scope chain)
-
ThisBinding - what
thispoints to
The value of this is determined by looking at how the function was called - specifically, the call-site.
The Four Rules of 'this' Binding
There are four main ways this gets bound, in order of precedence:
- new binding (constructor calls)
- Explicit binding (call, apply, bind)
- Implicit binding (method calls)
- Default binding (standalone function calls)
Let's dive into each one.
Rule 1: Default Binding (Standalone Function Calls)
When you call a function as a standalone function (not as a method, not with new, not with call/apply/bind), this defaults to the global object (or undefined in strict mode).
function showThis() {
console.log(this);
}
showThis(); // Window (in browser) or global (in Node.js)
What happens internally:
- JavaScript creates an Execution Context for
showThis - Looks at the call-site:
showThis()- no object, nonew, no explicit binding - Applies default binding:
ThisBinding = globalObject
In strict mode:
'use strict';
function showThis() {
console.log(this);
}
showThis(); // undefined
Strict mode changes the default binding from global to undefined. This prevents accidental global pollution.
Why your original example broke:
const user = {
name: 'Alice',
greet() {
console.log('Hello, ' + this.name);
}
};
const greet = user.greet;
greet(); // "Hello, undefined"
When you extract the method and call it standalone, it's just a regular function call. Default binding applies → this = globalObject (or undefined in strict mode) → this.name is undefined.
Rule 2: Implicit Binding (Method Calls)
When you call a function as a method of an object (using dot notation or bracket notation), this binds to that object.
const user = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
user.greet(); // "Alice"
What happens internally:
- JavaScript evaluates
user.greet- finds the function - Creates an Execution Context for that function
- Looks at the call-site:
user.greet()- called onuserobject - Sets
ThisBinding = user
The rule: The object immediately to the left of the dot (or bracket) becomes this.
Nested Objects: Only the Last Level Matters
const obj = {
level1: {
level2: {
method() {
console.log(this);
}
}
}
};
obj.level1.level2.method(); // this = level2 (not obj, not level1)
Only level2 (the immediate parent) becomes this, because that's what's directly to the left of the final dot before the call.
Implicit Binding Loss: The Common Trap
const user = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
setTimeout(user.greet, 1000); // "undefined" (or error in strict mode)
Why does this fail?
When you pass user.greet to setTimeout, you're passing a reference to the function, not calling it. Inside setTimeout, roughly this happens:
function setTimeout(callback, delay) {
// ... wait for delay ...
callback(); // Called as standalone function!
}
The function is called without an object, so default binding applies.
Solutions:
1. Arrow function wrapper:
setTimeout(() => user.greet(), 1000); // "Alice"
The arrow function calls user.greet() with the object, preserving implicit binding.
2. bind() (we'll cover this soon):
setTimeout(user.greet.bind(user), 1000); // "Alice"
Rule 3: Explicit Binding (call, apply, bind)
JavaScript gives you three methods to explicitly set this: call, apply, and bind.
call() and apply()
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const user = { name: 'Alice' };
greet.call(user, 'Hello', '!'); // "Hello, Alice!"
greet.apply(user, ['Hello', '!']); // "Hello, Alice!"
What happens with call():
-
callinvokes the function immediately - First argument (
user) becomesthis - Remaining arguments are passed to the function
call vs apply: Only difference is argument passing:
-
call: arguments listed individually -
apply: arguments as an array
Under the hood:
// Conceptually, call does this:
Function.prototype.call = function(thisArg, ...args) {
// Set this function's ThisBinding to thisArg
// Invoke the function with args
// Return the result
};
bind(): Creating a Hard-Bound Function
function greet() {
console.log(this.name);
}
const user = { name: 'Alice' };
const boundGreet = greet.bind(user);
boundGreet(); // "Alice"
// Even if you try to change this, it won't work:
const other = { name: 'Bob' };
boundGreet.call(other); // Still "Alice"!
What bind does:
bind returns a new function with this permanently bound. No matter how you call it later, this will always be what you bound it to.
Polyfill (simplified) to understand what bind does:
Function.prototype.bind = function(thisArg, ...fixedArgs) {
const originalFunction = this;
return function boundFunction(...callArgs) {
return originalFunction.apply(
thisArg,
[...fixedArgs, ...callArgs]
);
};
};
It returns a new function that, when called, invokes the original function with a fixed this value.
This is why bind solves the setTimeout problem:
setTimeout(user.greet.bind(user), 1000);
bind returns a new function that always calls user.greet with this = user, no matter how setTimeout calls it.
Rule 4: new Binding (Constructor Calls)
When you call a function with new, JavaScript does something special:
function User(name) {
this.name = name;
this.greet = function() {
console.log('Hello, ' + this.name);
};
}
const alice = new User('Alice');
alice.greet(); // "Hello, Alice"
What happens when you use new:
- Create a new empty object:
{} - Set the object's
[[Prototype]]toUser.prototype - Execute
Userwiththisbound to the new object - If
Userdoesn't return an object, return the new object
// Conceptually:
function User(name) {
// const this = {}; (done by 'new')
// this.[[Prototype]] = User.prototype; (done by 'new')
this.name = name;
this.greet = function() {
console.log('Hello, ' + this.name);
};
// return this; (done by 'new' if you don't return an object)
}
new has the highest precedence (except for explicit arrow function binding, which we'll cover next).
function test() {
console.log(this.value);
}
const obj = { value: 42 };
const boundTest = test.bind(obj);
boundTest(); // 42
new boundTest(); // undefined (creates new object, ignores bind)
Even though test was bound to obj, new creates a fresh object and binds this to that instead.
Arrow Functions: Lexical 'this'
Arrow functions completely break all these rules. They don't have their own this binding at all.
const user = {
name: 'Alice',
greet: () => {
console.log(this.name);
}
};
user.greet(); // undefined (or error)
Why?
Arrow functions use lexical this - they inherit this from the enclosing scope at the time they're defined, not when they're called.
const user = {
name: 'Alice',
greet: () => {
// 'this' here is the 'this' from the scope where this arrow function was created
// That's the global scope (where the object literal was evaluated)
console.log(this.name);
}
};
The arrow function was created in the global scope, so this inherits from there (global object or undefined).
When arrow functions are useful:
const user = {
name: 'Alice',
delays: [1000, 2000, 3000],
start() {
this.delays.forEach(delay => {
setTimeout(() => {
console.log(this.name + ' after ' + delay + 'ms');
}, delay);
});
}
};
user.start();
// "Alice after 1000ms"
// "Alice after 2000ms"
// "Alice after 3000ms"
The arrow functions in forEach and setTimeout don't create their own this - they use the this from start(), which is user (implicit binding).
Without arrow functions, you'd need:
start() {
const self = this; // Capture this
this.delays.forEach(function(delay) {
setTimeout(function() {
console.log(self.name + ' after ' + delay + 'ms');
}, delay);
});
}
Important: You can't change an arrow function's this with call, apply, or bind:
const arrow = () => console.log(this.name);
const obj = { name: 'Alice' };
arrow.call(obj); // this is still global/undefined, not obj
Precedence: When Rules Conflict
When multiple rules apply, here's the priority (highest to lowest):
-
Arrow function - always uses lexical
this, can't be overridden - new - creates new object and binds to it
-
Explicit binding (call/apply/bind) - you explicitly set
this - Implicit binding - method call on an object
- Default binding - global or undefined
Example:
function test() {
console.log(this.value);
}
const obj1 = { value: 1 };
const obj2 = { value: 2 };
const bound = test.bind(obj1);
bound.call(obj2); // 1 (bind wins over call)
bind creates a hard-bound function that can't be overridden by call.
Real-World Scenarios
Event Handlers in DOM
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this); // The button element
});
Browser event handlers set this to the element that triggered the event (implicit binding).
With arrow function:
button.addEventListener('click', () => {
console.log(this); // Window or undefined (lexical this)
});
Class Methods
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log('Hello, ' + this.name);
}
}
const alice = new User('Alice');
alice.greet(); // "Hello, Alice"
const greet = alice.greet;
greet(); // Error: Cannot read 'name' of undefined
Class methods are not automatically bound. To fix:
Solution 1: Arrow function (in constructor):
class User {
constructor(name) {
this.name = name;
this.greet = () => {
console.log('Hello, ' + this.name);
};
}
}
Solution 2: Bind in constructor:
class User {
constructor(name) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log('Hello, ' + this.name);
}
}
Solution 3: Class field (modern JavaScript):
class User {
name;
greet = () => {
console.log('Hello, ' + this.name);
}
constructor(name) {
this.name = name;
}
}
React Components (Classic Pattern)
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// Must bind or increment won't have correct 'this'
this.increment = this.increment.bind(this);
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.increment}>
Count: {this.state.count}
</button>
);
}
}
Without binding, this.increment passed to onClick loses its context (implicit binding loss).
Common Mistakes and Debugging
Mistake 1: Extracting methods
const obj = {
value: 42,
getValue() { return this.value; }
};
const getValue = obj.getValue;
console.log(getValue()); // undefined
Fix: Keep the object reference or use bind:
console.log(obj.getValue()); // 42
// or
const getValue = obj.getValue.bind(obj);
console.log(getValue()); // 42
Mistake 2: Callback context loss
const obj = {
values: [1, 2, 3],
double() {
return this.values.map(function(v) {
return v * this.multiplier; // this.multiplier is undefined!
});
},
multiplier: 2
};
Fix: Arrow function or bind:
double() {
return this.values.map(v => v * this.multiplier);
}
Mistake 3: Arrow functions as methods
const obj = {
value: 42,
getValue: () => this.value // this is NOT obj!
};
Fix: Use regular function for methods:
const obj = {
value: 42,
getValue() { return this.value; }
};
How to Debug 'this'
When this isn't what you expect:
- Find the call-site - where is the function actually called?
-
Check how it's called:
- Is it
object.method()? → implicit binding - Is it
func()? → default binding - Is it
new func()? → new binding - Is it
func.call/apply/bind? → explicit binding - Is it an arrow function? → lexical
this
- Is it
Apply the precedence rules
Add logging:
function test() {
console.log('this is:', this);
console.log('this.constructor.name:', this.constructor.name);
// Your actual code
}
Summary: 'this' is All About the Call-Site
Key takeaways:
Four binding rules (in precedence order):
-
new binding:
this= newly created object -
Explicit binding:
this= what you passed to call/apply/bind -
Implicit binding:
this= object before the dot -
Default binding:
this= global object (or undefined in strict mode)
Arrow functions:
- Don't have their own
this - Inherit
thisfrom enclosing scope (lexically) - Can't be changed with call/apply/bind
- Perfect for callbacks where you want to preserve
this
Common patterns:
- Use arrow functions for callbacks
- Use regular functions for methods
- Bind methods in constructors if passing them around
- Remember: extraction loses context
The golden rule: To know what this is, find where the function is called (not where it's defined), and apply the binding rules.
Next time you see unexpected this behavior, trace the call-site and ask: "How is this function being invoked?" The answer will tell you exactly what this should be.
If you have a different mental model of this, or noticed an edge case worth discussing — share it in the comments!
Top comments (0)