The `this` Keyword & Binding
Checking access...
The this keyword is one of the most misunderstood features in JavaScript. Unlike other languages where this always refers to the current object instance, JavaScript’s this is dynamic — it depends entirely on how a function is called, not where it’s defined.
The Five Binding Rules
this is determined by five rules, applied in order of precedence:
1. Default Binding (Global)
When a function is called as a plain function call (no dot, no special method), this refers to the global object (or undefined in strict mode).
function showThis() { console.log(this);}
showThis(); // In browser: Window (global object)With strict mode:
"use strict";function showThis() { console.log(this);}
showThis(); // undefinedCaution
Default binding is almost never what you want. It’s why accidentally calling a method without its object leads to bugs.
2. Implicit Binding (Method Call)
When a function is called as a method of an object (with a dot), 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"The key insight: only the immediate call site matters:
function sayName() { console.log(this.name);}
const user1 = { name: "Alice", sayName };const user2 = { name: "Bob", sayName };
user1.sayName(); // "Alice"user2.sayName(); // "Bob"The “lost this” problem — when you extract a method from its object:
const user = { name: "Alice", greet() { console.log(`Hi, I'm ${this.name}`); }};
const greetFn = user.greet;greetFn(); // "Hi, I'm undefined" — lost implicit binding!The function was called as a plain function, so default binding applies.
3. Explicit Binding (call, apply, bind)
You can force what this should be using three methods available on every function.
call — calls the function with a given this and arguments listed individually:
function introduce(greeting, punctuation) { console.log(`${greeting}, I'm ${this.name}${punctuation}`);}
const person = { name: "Charlie" };
introduce.call(person, "Hello", "!"); // "Hello, I'm Charlie!"apply — same as call but arguments are passed as an array:
introduce.apply(person, ["Hi", "!!"]); // "Hi, I'm Charlie!!"bind — returns a new function with this permanently bound:
const boundIntroduce = introduce.bind(person, "Hey");boundIntroduce("?"); // "Hey, I'm Charlie?"bind does not call the function immediately. It returns a new function with this locked.
4. new Binding (Constructor)
When a function is called with the new keyword, a new object is created and this points to that object:
function User(name) { // `this` is a brand-new object this.name = name; // no return — implicit return of `this`}
const alice = new User("Alice");console.log(alice.name); // "Alice"5. Arrow Functions (Lexical this)
Arrow functions do not have their own this. They capture this from the surrounding lexical scope at the time they are defined.
const user = { name: "Alice", greet: () => { console.log(`Hi, I'm ${this.name}`); }};
user.greet(); // "Hi, I'm undefined" — arrow function used here!The arrow function captured this from the surrounding scope (the global scope), not from user.
Where arrow functions shine — inside methods that need to reference the parent this:
const timer = { seconds: 0, start() { setInterval(() => { this.seconds++; // `this` captured from start()'s scope → timer object console.log(this.seconds); }, 1000); }};
timer.start(); // 1, 2, 3...Without the arrow function, you’d need a workaround:
// Old approach: capture `this` in a variablestart() { const self = this; setInterval(function() { self.seconds++; }, 1000);}
// Or use .bindstart() { setInterval(function() { this.seconds++; }.bind(this), 1000);}Binding Precedence
When multiple rules could apply, this is the order:
newbinding — highest priority- Explicit binding (
call/apply/bind) - Implicit binding (method call)
- Default binding — lowest priority
function identify() { console.log(this.name);}
const obj1 = { name: "obj1" };const obj2 = { name: "obj2" };
// bind wins over implicitconst boundFn = identify.bind(obj1);boundFn.call(obj2); // "obj1" — bind is permanent
// new wins over bindfunction User(name) { this.name = name;}const boundUser = User.bind({ name: "ignored" });const instance = new boundUser("Alice");console.log(instance.name); // "Alice" — new overrides bindEvent Handlers and this
In DOM event handlers, this refers to the element that fired the event:
button.addEventListener("click", function() { console.log(this); // the button element});With arrow functions, this is captured from the surrounding scope:
button.addEventListener("click", () => { console.log(this); // NOT the button — probably Window/undefined});Common Pitfalls
1. Method Reference Loss
const user = { name: "Alice", greet() { console.log(`Hi, I'm ${this.name}`); }};
// Problem: passing method as a callbacksetTimeout(user.greet, 1000); // "Hi, I'm undefined"
// Fix 1: wrap in arrow functionsetTimeout(() => user.greet(), 1000);
// Fix 2: bind the methodsetTimeout(user.greet.bind(user), 1000);2. Inner Function in Method
const obj = { data: [1, 2, 3], process() { // `this` is obj here this.data.forEach(function(item) { console.log(this); // undefined (strict) or Window (sloppy) — not obj! }); }};
// Fix 1: arrow functionthis.data.forEach((item) => console.log(this));
// Fix 2: pass thisArgthis.data.forEach(function(item) { console.log(this);}, this);
// Fix 3: capture thisconst self = this;this.data.forEach(function(item) { console.log(self);});3. Class Methods as Callbacks
class Button { constructor(label) { this.label = label; }
handleClick() { console.log(`${this.label} clicked`); }}
const btn = new Button("Save");document.querySelector("button").addEventListener("click", btn.handleClick);// "undefined clicked" — `this` is the button element
// Fix: bind in constructorclass Button { constructor(label) { this.label = label; this.handleClick = this.handleClick.bind(this); } // or use class field with arrow function: handleClick = () => { console.log(`${this.label} clicked`); }}Quick Reference
| Call Pattern | this Value |
|---|---|
Plain function call fn() | window (sloppy) / undefined (strict) |
Method call obj.fn() | obj |
.call(ctx) / .apply(ctx) | ctx |
.bind(ctx) | ctx (permanent) |
new Fn() | New empty object |
| Arrow function | Lexical (surrounding scope) |
Event handler function() | Element that fired event |
Event handler () => | Lexical scope |
Practice Exercises
What does this log?
const obj = {name: "obj",greet() {console.log(this.name);}};const fn = obj.greet;fn();Fix this timer without changing
setInterval:class Timer {constructor() {this.seconds = 0;}start() {setInterval(function() {this.seconds++;console.log(this.seconds);}, 1000);}}new Timer().start();Create a
partialfunction that works likebindbut also allows additional arguments later:function partial(fn, ...args) {// your code here}function add(a, b, c) {return a + b + c;}const add5 = partial(add, 5);console.log(add5(10, 20)); // 35