DEV Community

Tihomir Ivanov
Tihomir Ivanov

Posted on

The `this` Keyword & Binding: JavaScript's Most Confusing Feature (Finally Explained)

Ask 10 JavaScript developers what this means, and you'll get 10 different explanations. It's the source of countless bugs, especially for developers coming from other languages where this (or self) is straightforward.

Here's the uncomfortable truth: In JavaScript, this isn't determined by where a function is defined — it's determined by how the function is called.

This single fact explains 90% of the confusion.

The Golden Rule

The value of this is determined by the call site (how the function is invoked), not where the function is written. There are four binding rules, and arrow functions break all of them by lexically binding this to their surrounding scope.

Let's break down each rule systematically.


Part 1: The Four Binding Rules

Rule 1: Default Binding (Function Invocation)

When a function is called standalone (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(); // In browser: Window, in Node: global, in strict mode: undefined
Enter fullscreen mode Exit fullscreen mode

In strict mode:

'use strict';

function showThis() {
  console.log(this);
}

showThis(); // undefined
Enter fullscreen mode Exit fullscreen mode

Key Point: This is the "fallback" rule when no other rule applies.


Rule 2: Implicit Binding (Method Invocation)

When a function is called as a method of an object, this refers to the object before the dot.

const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

person.greet(); // "Hello, I'm Alice"
Enter fullscreen mode Exit fullscreen mode

The rule: Look at the call site. What's to the left of the dot? That's this.


Common Mistake: Losing Implicit Binding

const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

const greet = person.greet; // Extract the function
greet(); // "Hello, I'm undefined" (or error in strict mode)
Enter fullscreen mode Exit fullscreen mode

Why? When you call greet() (not person.greet()), there's no object before the dot, so default binding applies, not implicit binding.

This also happens with callbacks:

setTimeout(person.greet, 1000); // Loses 'this'
Enter fullscreen mode Exit fullscreen mode

Fix:

setTimeout(() => person.greet(), 1000); // Call it as a method
// or
setTimeout(person.greet.bind(person), 1000); // Explicitly bind
Enter fullscreen mode Exit fullscreen mode

Rule 3: Explicit Binding (call, apply, bind)

You can explicitly set this using call(), apply(), or bind().

call() and apply()

function greet(greeting, punctuation) {
  console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!');  // "Hello, I'm Alice!"
greet.apply(person, ['Hi', '.']);  // "Hi, I'm Alice."
Enter fullscreen mode Exit fullscreen mode

Difference:

  • call(thisArg, arg1, arg2, ...) — arguments passed individually
  • apply(thisArg, [arg1, arg2, ...]) — arguments passed as array

bind()

function greet() {
  console.log(`Hello, I'm ${this.name}`);
}

const person = { name: 'Alice' };

const boundGreet = greet.bind(person); // Returns NEW function with 'this' locked

boundGreet(); // "Hello, I'm Alice"

setTimeout(boundGreet, 1000); // 'this' is still 'person'
Enter fullscreen mode Exit fullscreen mode

Key Point: bind() returns a new function with this permanently set. Useful for callbacks.


Rule 4: new Binding (Constructor Invocation)

When you call a function with new, JavaScript:

  1. Creates a new empty object
  2. Sets the object's [[Prototype]] to the function's prototype
  3. Binds this to the new object
  4. Returns the object (unless the function explicitly returns a different object)
function Person(name) {
  this.name = name; // 'this' refers to the new object
  console.log(this);
}

const alice = new Person('Alice');
// Logs: Person { name: 'Alice' }

console.log(alice.name); // "Alice"
Enter fullscreen mode Exit fullscreen mode

What new does behind the scenes:

function Person(name) {
  // const this = Object.create(Person.prototype); (implicit)
  this.name = name;
  // return this; (implicit)
}
Enter fullscreen mode Exit fullscreen mode

What if a constructor returns an object?

function Person(name) {
  this.name = name;
  return { different: true }; // Explicit return
}

const alice = new Person('Alice');
console.log(alice); // { different: true } (not Person instance!)
Enter fullscreen mode Exit fullscreen mode

Rule: If a constructor returns an object, that object is returned instead of this. If it returns a primitive, this is returned.


Part 2: Arrow Functions (Breaking the Rules)

Arrow functions don't have their own this. Instead, they lexically inherit this from their surrounding scope.

const person = {
  name: 'Alice',
  greet: function() {
    const inner = () => {
      console.log(this.name); // 'this' is inherited from greet()
    };
    inner();
  }
};

person.greet(); // "Alice"
Enter fullscreen mode Exit fullscreen mode

Key Point: The arrow function doesn't care how it's called — it captures this from where it's defined.


Arrow Functions vs Regular Functions

const person = {
  name: 'Alice',
  regularGreet: function() {
    console.log(this.name);
  },
  arrowGreet: () => {
    console.log(this.name); // 'this' is NOT 'person'!
  }
};

person.regularGreet(); // "Alice" (implicit binding)
person.arrowGreet();   // undefined (lexical binding to outer scope, probably global)
Enter fullscreen mode Exit fullscreen mode

Why? The arrow function was defined in the global scope (outside any function), so its this is the global object (or undefined in modules).


When Arrow Functions are Perfect

Avoiding this confusion in callbacks:

const person = {
  name: 'Alice',
  hobbies: ['reading', 'coding'],
  showHobbies: function() {
    this.hobbies.forEach(hobby => {
      console.log(`${this.name} likes ${hobby}`); // 'this' is 'person'
    });
  }
};

person.showHobbies();
// "Alice likes reading"
// "Alice likes coding"
Enter fullscreen mode Exit fullscreen mode

With a regular function (old way):

showHobbies: function() {
  const self = this; // Save reference
  this.hobbies.forEach(function(hobby) {
    console.log(`${self.name} likes ${hobby}`); // Use 'self'
  });
}
Enter fullscreen mode Exit fullscreen mode

Arrow functions eliminate the need for self = this hacks.


Part 3: Binding Precedence

When multiple rules apply, what wins?

Precedence (highest to lowest):

  1. Arrow functions (lexical this, ignores all other rules)
  2. new binding (new always wins)
  3. Explicit binding (call, apply, bind)
  4. Implicit binding (method call)
  5. Default binding (standalone function call)

Example: new vs bind

function Person(name) {
  this.name = name;
}

const boundPerson = Person.bind({ name: 'Bound' });

const instance = new boundPerson('Alice');
console.log(instance.name); // "Alice" (new wins over bind)
Enter fullscreen mode Exit fullscreen mode

Example: Explicit vs Implicit

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

function greet() {
  console.log(this.name);
}

person1.greet = greet;
person1.greet.call(person2); // "Bob" (explicit wins over implicit)
Enter fullscreen mode Exit fullscreen mode

Part 4: this in React

Understanding this is critical for React class components (and understanding legacy code).

1. Class Components and Method Binding

The problem:

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
  }

  handleClick() {
    this.setState({ clicked: true }); // 'this' is undefined!
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Why does this fail?

  • When React calls the event handler, it's a standalone function call (default binding)
  • In strict mode (React uses strict mode), this is undefined

Solutions

Solution 1: Arrow function property (modern)

class Button extends React.Component {
  handleClick = () => { // Arrow function
    this.setState({ clicked: true });
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

How it works: Arrow functions lexically bind this to the class instance.

Trade-off: Creates a new function per instance (not on prototype), slightly more memory.


Solution 2: Bind in constructor

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); // Bind once
  }

  handleClick() {
    this.setState({ clicked: true });
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

How it works: bind() creates a new function with this locked to the instance.


Solution 3: Arrow function in JSX (not recommended)

class Button extends React.Component {
  handleClick() {
    this.setState({ clicked: true });
  }

  render() {
    return <button onClick={() => this.handleClick()}>Click me</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Why not recommended: Creates a new function on every render, which can cause unnecessary re-renders in child components.


2. Functional Components (No this Problems!)

Modern React with hooks eliminates this entirely:

function Button() {
  const [clicked, setClicked] = useState(false);

  const handleClick = () => {
    setClicked(true); // No 'this' needed!
  };

  return <button onClick={handleClick}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Why this is cleaner:

  • No this binding issues
  • No need for constructors
  • Closures handle state access (see Closures article!)

3. Passing this Context to Children

In class components:

class Parent extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <Child onIncrement={this.increment} />;
  }
}

function Child({ onIncrement }) {
  return <button onClick={onIncrement}>Increment</button>;
}
Enter fullscreen mode Exit fullscreen mode

The increment arrow function ensures this is always the Parent instance, even when called from Child.


4. Event Handlers and Synthetic Events

React's event system preserves this in interesting ways:

class Form extends React.Component {
  state = { value: '' };

  handleChange(event) { // Regular method
    this.setState({ value: event.target.value }); // 'this' works!
  }

  render() {
    return (
      <input
        value={this.state.value}
        onChange={this.handleChange.bind(this)} // Must bind!
      />
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Why does this work differently than other callbacks?

  • It doesn't! You still need to bind
  • The difference is React automatically binds some lifecycle methods (render, constructor)

Part 5: Common this Gotchas

Gotcha 1: Nested Function Calls

const obj = {
  value: 42,
  getValue: function() {
    function inner() {
      return this.value; // 'this' is undefined (strict mode)
    }
    return inner();
  }
};

console.log(obj.getValue()); // undefined or error
Enter fullscreen mode Exit fullscreen mode

Fix:

getValue: function() {
  const inner = () => this.value; // Arrow function
  return inner();
}
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: this in Object Literals

const obj = {
  value: 42,
  getValue: function() {
    return this.value; // Works
  },
  getValueArrow: () => {
    return this.value; // 'this' is NOT obj!
  }
};

console.log(obj.getValue());      // 42
console.log(obj.getValueArrow()); // undefined
Enter fullscreen mode Exit fullscreen mode

Why? The arrow function is defined in the global scope, so its this is global (or undefined in modules).


Gotcha 3: this in Async Functions

const obj = {
  value: 42,
  async getValue() {
    return this.value; // 'this' is preserved
  }
};

obj.getValue().then(console.log); // 42
Enter fullscreen mode Exit fullscreen mode

Good news: async/await doesn't change this behavior — it follows the same binding rules.


Gotcha 4: this in Classes with Callbacks

class Counter {
  constructor() {
    this.count = 0;
  }

  increment() {
    this.count++;
  }

  start() {
    setInterval(this.increment, 1000); // 'this' is lost!
  }
}

const counter = new Counter();
counter.start(); // Error: Cannot read 'count' of undefined
Enter fullscreen mode Exit fullscreen mode

Fix:

start() {
  setInterval(() => this.increment(), 1000); // Arrow function
  // or
  setInterval(this.increment.bind(this), 1000); // Bind
}
Enter fullscreen mode Exit fullscreen mode

Quick Reference Cheat Sheet

Binding Type Call Site Example this Value
Default func() undefined (strict mode) or global
Implicit obj.func() obj
Explicit func.call(obj) obj
new new Func() New instance
Arrow () => {} Lexical (outer scope)

Key Takeaways

this is determined by how a function is called, not where it's defined
Four binding rules: default, implicit, explicit, new (in order of precedence)
Arrow functions lexically bind this from their surrounding scope
Losing this in callbacks is the most common bug — use arrow functions or bind()
In React class components, bind methods in the constructor or use arrow functions
Functional components with hooks avoid this entirely (one reason they're preferred)
bind() creates a new function; call() and apply() invoke immediately


Interview Tip

When asked about this, explain it systematically:

  1. "The value of this depends on how a function is called, not where it's defined"
  2. Walk through the four rules: default, implicit, explicit, new
  3. Mention arrow functions: "Arrow functions don't have their own this — they inherit it lexically"
  4. React example: "In class components, event handlers need binding because they're called as standalone functions, losing the implicit binding to the class instance"
  5. Functional components: "Modern React avoids this entirely with hooks and closures"

Now go forth and never lose this again!

Top comments (0)