JavaScript — The `this` Keyword
The this keyword is one of the most misunderstood concepts in JavaScript. Unlike other languages where this always refers to the current instance of a class, in JavaScript the value of this is determined by how a function is called, not where it is defined.
There are five binding rules that determine the value of this: default binding, implicit binding, explicit binding, new binding, and lexical binding with arrow functions. Understanding these rules and their precedence is essential for writing correct JavaScript.
Each call site — the location in code where a function is invoked — determines what this will be. The same function can have different this values depending on how it is called. This dynamic nature is powerful but requires careful attention.
| Binding Rule | How this is Determined | Example Call |
|---|---|---|
| Default | Global object (window) or undefined in strict mode | foo() |
| Implicit | The object before the dot | obj.foo() |
| Explicit | Value passed to call/apply/bind | foo.call(obj) |
| new | The newly created instance | new Foo() |
| Lexical (Arrow) | this from the enclosing scope | () => this.x |
Default binding applies when a function is called as a standalone function — no object, no new, no explicit binding. In non-strict mode, this defaults to the global object (window in browsers, global in Node.js). In strict mode, this is undefined.
| 1 | // Non-strict mode — default binding binds to global object |
| 2 | function showThis() { |
| 3 | console.log(this); // window (in browser) |
| 4 | } |
| 5 | showThis(); |
| 6 | |
| 7 | // Strict mode — default binding is undefined |
| 8 | function strictShow() { |
| 9 | "use strict"; |
| 10 | console.log(this); // undefined |
| 11 | } |
| 12 | strictShow(); |
| 13 | |
| 14 | // Common pitfall: method extracted from its object |
| 15 | const user = { |
| 16 | name: "Alice", |
| 17 | greet() { |
| 18 | console.log(`Hello, I'm ${this.name}`); |
| 19 | } |
| 20 | }; |
| 21 | |
| 22 | const greetFn = user.greet; // Extract method reference |
| 23 | greetFn(); // Default binding applies — this is undefined or window |
| 24 | // "Hello, I'm undefined" (strict) or "Hello, I'm " (non-strict) |
| 25 | |
| 26 | // Same issue with passing methods as callbacks |
| 27 | setTimeout(user.greet, 100); // this lost! |
| 28 | // Fix: wrap in arrow function or use bind |
| 29 | setTimeout(() => user.greet(), 100); // works |
| 30 | setTimeout(user.greet.bind(user), 100); // also works |
| 31 | |
| 32 | // Nested function — inner function gets default binding |
| 33 | const counter = { |
| 34 | count: 0, |
| 35 | start() { |
| 36 | function increment() { |
| 37 | this.count++; // this is window or undefined, NOT counter |
| 38 | } |
| 39 | increment(); // default binding! |
| 40 | } |
| 41 | }; |
| 42 | // Fix: capture this, use arrow, or bind |
warning
When a function is called as a method of an object (using the dot notation or bracket notation), this is bound to the object that owns the method call. The rule is simple: look at the left side of the dot at the call site.
| 1 | // Implicit binding — this is the object before the dot |
| 2 | const person = { |
| 3 | name: "Bob", |
| 4 | greet() { |
| 5 | console.log(`Hi, I'm ${this.name}`); |
| 6 | } |
| 7 | }; |
| 8 | person.greet(); // "Hi, I'm Bob" — this is person |
| 9 | |
| 10 | // Bracket notation also uses implicit binding |
| 11 | const method = "greet"; |
| 12 | person[method](); // "Hi, I'm Bob" — same rule |
| 13 | |
| 14 | // Chained property access — only the last level matters |
| 15 | const obj1 = { |
| 16 | name: "obj1", |
| 17 | child: { |
| 18 | name: "obj2", |
| 19 | greet() { |
| 20 | console.log(this.name); |
| 21 | } |
| 22 | } |
| 23 | }; |
| 24 | obj1.child.greet(); // "obj2" — this is obj1.child (the last object) |
| 25 | |
| 26 | // Implicit binding is lost in indirection |
| 27 | const greet = person.greet; |
| 28 | greet(); // default binding! this is not person |
| 29 | |
| 30 | // Implicitly bound functions passed as arguments lose this |
| 31 | function callTwice(fn) { |
| 32 | fn(); |
| 33 | fn(); |
| 34 | } |
| 35 | callTwice(person.greet); // default binding both times |
| 36 | |
| 37 | // Context loss with object destructuring |
| 38 | const { greet } = person; |
| 39 | greet(); // default binding — this is not person |
| Call Pattern | this Value | Example |
|---|---|---|
| Standalone call | Default binding | foo() |
| Method call (dot) | Object before dot | obj.foo() |
| Method reference | Default binding (lost!) | const f = obj.foo; f() |
| Callback passed | Default binding (lost!) | setTimeout(obj.foo, 100) |
JavaScript gives you direct control over this via call, apply, and bind. These methods allow you to explicitly set the this value for a function invocation, overriding whatever implicit or default binding would have applied.
call and apply
call and apply invoke the function immediately with a specified this. The only difference is how arguments are passed: call takes comma-separated arguments, apply takes an array of arguments.
| 1 | // call — explicit this + comma-separated arguments |
| 2 | function introduce(greeting, punctuation) { |
| 3 | console.log(`${greeting}, I'm ${this.name}${punctuation}`); |
| 4 | } |
| 5 | |
| 6 | const alice = { name: "Alice" }; |
| 7 | const bob = { name: "Bob" }; |
| 8 | |
| 9 | introduce.call(alice, "Hello", "!"); // "Hello, I'm Alice!" |
| 10 | introduce.call(bob, "Hey", "."); // "Hey, I'm Bob." |
| 11 | |
| 12 | // apply — same but arguments as array |
| 13 | introduce.apply(alice, ["Hi", "!!!"]); // "Hi, I'm Alice!!!" |
| 14 | |
| 15 | // Practical: Math.max with an array |
| 16 | const numbers = [3, 7, 1, 9, 4]; |
| 17 | console.log(Math.max.apply(null, numbers)); // 9 |
| 18 | // Modern: Math.max(...numbers) |
| 19 | |
| 20 | // Practical: array-like to array conversion (pre-ES6) |
| 21 | function argsToArray() { |
| 22 | return Array.prototype.slice.apply(arguments); |
| 23 | // or: Array.from(arguments) |
| 24 | } |
| 25 | |
| 26 | // First argument to call/apply sets this |
| 27 | // If null/undefined passed, default binding applies (global or undefined in strict) |
bind
Unlike call and apply, bind does not invoke the function. Instead, it creates a new function with the this value permanently bound. Once a function is bound, its this cannot be overridden — even by call or apply.
| 1 | // bind — creates a new function with fixed this |
| 2 | function greet() { |
| 3 | console.log(`Hello, ${this.name}`); |
| 4 | } |
| 5 | |
| 6 | const user = { name: "Charlie" }; |
| 7 | const boundGreet = greet.bind(user); |
| 8 | boundGreet(); // "Hello, Charlie" |
| 9 | |
| 10 | // Partial application — bind also pre-fills arguments |
| 11 | function multiply(a, b) { |
| 12 | return a * b; |
| 13 | } |
| 14 | const double = multiply.bind(null, 2); // permanently set first arg to 2 |
| 15 | console.log(double(5)); // 10 (2 * 5) |
| 16 | const triple = multiply.bind(null, 3); |
| 17 | console.log(triple(5)); // 15 |
| 18 | |
| 19 | // Bound this cannot be overridden |
| 20 | const other = { name: "Diana" }; |
| 21 | boundGreet.call(other); // Still "Hello, Charlie" — bind is permanent |
| 22 | boundGreet.apply(other); // Still "Hello, Charlie" |
| 23 | |
| 24 | // Practical: fixing this in callbacks |
| 25 | const button = { |
| 26 | text: "Click me", |
| 27 | listeners: [], |
| 28 | addListener(fn) { |
| 29 | this.listeners.push(fn); |
| 30 | }, |
| 31 | onClick() { |
| 32 | console.log(`Button "${this.text}" clicked`); |
| 33 | } |
| 34 | }; |
| 35 | |
| 36 | // Without bind — this is lost |
| 37 | const onClick = button.onClick; |
| 38 | // onClick(); // this is undefined or window |
| 39 | |
| 40 | // With bind — this is preserved |
| 41 | const boundOnClick = button.onClick.bind(button); |
| 42 | boundOnClick(); // "Button "Click me" clicked" |
| 43 | |
| 44 | // bind in event handlers |
| 45 | element.addEventListener("click", button.onClick.bind(button)); |
pro tip
Arrow functions do not have their own this. Instead, they inherit this from the enclosing lexical scope — the surrounding non-arrow function at the time the arrow function is defined. This is called lexical binding. Arrow functions cannot be used with new, and call/apply/bind cannot change their this.
| 1 | // Arrow functions inherit this from enclosing scope |
| 2 | const obj = { |
| 3 | name: "Object", |
| 4 | regular: function() { |
| 5 | console.log("regular:", this.name); // this is obj |
| 6 | |
| 7 | const arrow = () => { |
| 8 | console.log("arrow:", this.name); // this is also obj (inherited) |
| 9 | }; |
| 10 | arrow(); |
| 11 | }, |
| 12 | arrowMethod: () => { |
| 13 | console.log("arrowMethod:", this.name); // this is NOT obj! |
| 14 | // Arrow methods on objects use enclosing scope (global/window) |
| 15 | } |
| 16 | }; |
| 17 | |
| 18 | obj.regular(); // regular: Object / arrow: Object |
| 19 | obj.arrowMethod(); // arrowMethod: undefined (wrong!) |
| 20 | |
| 21 | // Arrow functions fix the "lost this" in nested functions |
| 22 | const timer = { |
| 23 | count: 0, |
| 24 | start() { |
| 25 | // Old way — capture this |
| 26 | const self = this; |
| 27 | setInterval(function() { |
| 28 | self.count++; |
| 29 | }, 1000); |
| 30 | |
| 31 | // Better — arrow function inherits this from start() |
| 32 | setInterval(() => { |
| 33 | this.count++; // this is timer (inherited from start) |
| 34 | }, 1000); |
| 35 | } |
| 36 | }; |
| 37 | |
| 38 | // Arrow functions in array methods preserve this |
| 39 | const team = { |
| 40 | name: "Dev Team", |
| 41 | members: ["Alice", "Bob"], |
| 42 | printMembers() { |
| 43 | this.members.forEach((member) => { |
| 44 | // Arrow inherits this from printMembers |
| 45 | console.log(`${this.name}: ${member}`); |
| 46 | }); |
| 47 | } |
| 48 | }; |
| 49 | team.printMembers(); // "Dev Team: Alice", "Dev Team: Bob" |
warning
When a function is invoked with the new keyword, a new object is created, and this is bound to that new object. The new object is returned automatically unless the constructor returns a non-primitive object.
| 1 | // new binding — this is the new instance |
| 2 | function Person(name) { |
| 3 | // this = newly created object |
| 4 | this.name = name; |
| 5 | this.greet = function() { |
| 6 | console.log(`Hi, I'm ${this.name}`); |
| 7 | }; |
| 8 | // return this; (implicit) |
| 9 | } |
| 10 | |
| 11 | const alice = new Person("Alice"); |
| 12 | alice.greet(); // "Hi, I'm Alice" |
| 13 | console.log(alice instanceof Person); // true |
| 14 | |
| 15 | // new binding takes precedence over implicit binding |
| 16 | function Dog(name) { |
| 17 | this.name = name; |
| 18 | } |
| 19 | |
| 20 | const dog1 = new Dog("Rex"); |
| 21 | const obj = { Dog: Dog }; |
| 22 | const dog2 = new obj.Dog("Spot"); // new binding wins over implicit |
| 23 | console.log(dog2.name); // "Spot" |
| 24 | |
| 25 | // new binding also beats explicit binding (bind) |
| 26 | function Foo() { |
| 27 | console.log(this instanceof Foo); // true when called with new |
| 28 | } |
| 29 | const boundFoo = Foo.bind({}); |
| 30 | new boundFoo(); // this is a new Foo instance, NOT the bound object |
| 31 | // new binding: highest precedence (after arrow) |
| 32 | |
| 33 | // Constructor return value: |
| 34 | // If constructor returns an object, that object is used instead of this |
| 35 | function ReturnsObject() { |
| 36 | this.name = "default"; |
| 37 | return { name: "override" }; |
| 38 | } |
| 39 | const instance = new ReturnsObject(); |
| 40 | console.log(instance.name); // "override" — returned object wins |
| 41 | |
| 42 | // If constructor returns a primitive, it's ignored |
| 43 | function ReturnsPrimitive() { |
| 44 | this.name = "real"; |
| 45 | return 42; // primitive — ignored |
| 46 | } |
| 47 | const instance2 = new ReturnsPrimitive(); |
| 48 | console.log(instance2.name); // "real" |
| Rule | Precedence | Can be Overridden? |
|---|---|---|
| Arrow function | Highest (cannot change) | No |
| new | Second | No (if bind applied before new, new wins) |
| Explicit (call/apply/bind) | Third | Beats implicit, not new or arrow |
| Implicit (method call) | Fourth | Beats default |
| Default | Lowest | Always overridden by any other rule |
In DOM event handlers, this behaves differently depending on how the handler is registered. With addEventListener, this refers to the element the listener is attached to (currentTarget). Inline handlers and arrow functions have their own behaviors.
| 1 | // addEventListener — this is the element |
| 2 | const button = document.getElementById("myButton"); |
| 3 | button.addEventListener("click", function() { |
| 4 | console.log(this); // <button> element |
| 5 | console.log(this.id); // "myButton" |
| 6 | console.log(this === e.currentTarget); // true |
| 7 | }); |
| 8 | |
| 9 | // Arrow function in addEventListener — this is lexical |
| 10 | button.addEventListener("click", (e) => { |
| 11 | console.log(this); // NOT the button! (window or undefined) |
| 12 | // Use e.currentTarget instead |
| 13 | console.log(e.currentTarget.id); // "myButton" |
| 14 | }); |
| 15 | |
| 16 | // Inline handler — this is the element |
| 17 | // <button onclick="console.log(this)">Click</button> |
| 18 | // this is the <button> element |
| 19 | |
| 20 | // Inline handler with strict module |
| 21 | // <button onclick="handleClick()">Click</button> |
| 22 | // handleClick() gets default binding — this is undefined in strict mode |
| 23 | |
| 24 | // Class method as event handler — this gets lost! |
| 25 | class MyComponent { |
| 26 | constructor(element) { |
| 27 | this.element = element; |
| 28 | this.count = 0; |
| 29 | |
| 30 | // BAD — this will be the element when called |
| 31 | element.addEventListener("click", this.handleClick); |
| 32 | |
| 33 | // GOOD — bound |
| 34 | element.addEventListener("click", this.handleClick.bind(this)); |
| 35 | |
| 36 | // GOOD — arrow function wrapper |
| 37 | element.addEventListener("click", (e) => this.handleClick(e)); |
| 38 | |
| 39 | // BEST — class property arrow (see bind vs arrow section) |
| 40 | } |
| 41 | |
| 42 | handleClick() { |
| 43 | this.count++; |
| 44 | console.log(`Clicked ${this.count} times`); |
| 45 | } |
| 46 | } |
info
In JavaScript classes, method this is lost when the method is extracted and used as a callback. Two common solutions exist: binding in the constructor or using class property arrow functions. Each has tradeoffs in terms of performance, inheritance, and testability.
| 1 | // Approach 1: bind in constructor |
| 2 | class Toggle1 { |
| 3 | constructor() { |
| 4 | this.active = false; |
| 5 | this.handleClick = this.handleClick.bind(this); |
| 6 | } |
| 7 | |
| 8 | handleClick() { |
| 9 | this.active = !this.active; |
| 10 | console.log(`Active: ${this.active}`); |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | // Approach 2: Class property arrow function |
| 15 | class Toggle2 { |
| 16 | active = false; |
| 17 | |
| 18 | handleClick = () => { |
| 19 | this.active = !this.active; |
| 20 | console.log(`Active: ${this.active}`); |
| 21 | }; |
| 22 | } |
| 23 | |
| 24 | // Approach 3: Arrow wrapper (creates new function each render) |
| 25 | class Toggle3 { |
| 26 | active = false; |
| 27 | |
| 28 | handleClick() { |
| 29 | this.active = !this.active; |
| 30 | } |
| 31 | |
| 32 | render() { |
| 33 | // BAD — new function every render |
| 34 | return <button onClick={() => this.handleClick()} />; |
| 35 | |
| 36 | // BETTER — stable reference |
| 37 | // return <button onClick={this.handleClick} />; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // Tradeoffs: |
| 42 | // bind — method on prototype, one function per instance, works with super |
| 43 | // arrow class property — NOT on prototype, one function per instance, no super.method() |
| 44 | |
| 45 | // Arrow class property — no prototype method |
| 46 | class Parent { |
| 47 | greet = () => console.log("Parent"); |
| 48 | } |
| 49 | class Child extends Parent { |
| 50 | greet = () => { |
| 51 | // super.greet(); // ERROR — greet is not on prototype |
| 52 | console.log("Child"); |
| 53 | }; |
| 54 | } |
| 55 | |
| 56 | // bind — works with prototype chain |
| 57 | class Parent2 { |
| 58 | constructor() { |
| 59 | this.greet = this.greet.bind(this); |
| 60 | } |
| 61 | greet() { console.log("Parent"); } |
| 62 | } |
| 63 | class Child2 extends Parent2 { |
| 64 | greet() { |
| 65 | super.greet(); // works! |
| 66 | console.log("Child"); |
| 67 | } |
| 68 | } |
| Aspect | bind in constructor | Class property arrow |
|---|---|---|
| Prototype | Method on prototype | Own property, not on prototype |
| super.method() | Supported | Not supported |
| Mock in tests | Easy (prototype) | Per-instance |
| Memory | One bound copy per instance | One arrow per instance |
1. Losing this in Nested Functions
| 1 | function Outer() { |
| 2 | this.value = 42; |
| 3 | |
| 4 | function inner() { |
| 5 | console.log(this.value); // undefined — this is global/undefined |
| 6 | } |
| 7 | inner(); |
| 8 | } |
| 9 | new Outer(); // undefined |
| 10 | |
| 11 | // Fix: capture this in a variable |
| 12 | function OuterFixed() { |
| 13 | this.value = 42; |
| 14 | const self = this; |
| 15 | |
| 16 | function inner() { |
| 17 | console.log(self.value); // 42 |
| 18 | } |
| 19 | inner(); |
| 20 | } |
| 21 | |
| 22 | // Fix: use arrow function |
| 23 | function OuterFixed2() { |
| 24 | this.value = 42; |
| 25 | const inner = () => { |
| 26 | console.log(this.value); // 42 |
| 27 | }; |
| 28 | inner(); |
| 29 | } |
2. this in setTimeout/setInterval
| 1 | function Timer() { |
| 2 | this.seconds = 0; |
| 3 | |
| 4 | // BAD — this is the timer object itself... no wait |
| 5 | setInterval(function() { |
| 6 | this.seconds++; // this is window/global, not Timer! |
| 7 | }, 1000); |
| 8 | |
| 9 | // FIX 1: capture this |
| 10 | const self = this; |
| 11 | setInterval(function() { |
| 12 | self.seconds++; |
| 13 | }, 1000); |
| 14 | |
| 15 | // FIX 2: bind |
| 16 | setInterval(function() { |
| 17 | this.seconds++; |
| 18 | }.bind(this), 1000); |
| 19 | |
| 20 | // FIX 3: arrow function |
| 21 | setInterval(() => { |
| 22 | this.seconds++; |
| 23 | }, 1000); |
| 24 | } |
3. this in Array Methods
| 1 | const data = { |
| 2 | values: [1, 2, 3], |
| 3 | multiplier: 2, |
| 4 | multiply() { |
| 5 | // BAD — function callback loses this |
| 6 | return this.values.map(function(v) { |
| 7 | return v * this.multiplier; // this is undefined/window |
| 8 | }); |
| 9 | }, |
| 10 | multiplyFixed() { |
| 11 | // GOOD — arrow preserves this |
| 12 | return this.values.map((v) => v * this.multiplier); |
| 13 | } |
| 14 | }; |
| 15 | |
| 16 | // 4. this with destructuring |
| 17 | function greet() { |
| 18 | console.log(`Hello, ${this.name}`); |
| 19 | } |
| 20 | const { name } = { name: "Test" }; |
| 21 | greet(); // this is not the destructured object |
| 22 | |
| 23 | // 5. Strict vs non-strict mode |
| 24 | // In modules (ESM), strict mode is always on |
| 25 | // All class bodies are strict by default |
| 26 | // "use strict" at the top of a file enables it globally for that file |
| 27 | |
| 28 | // 6. this in eval (avoid eval) |
| 29 | function evalTest() { |
| 30 | const x = eval("this"); |
| 31 | console.log(x); // depends on strict mode context |
| 32 | } |
Quick Reference — How to Fix this:
danger