DEV Community

Cover image for Understanding 'this' in JavaScript: Execution Context Explained
Razumovsky
Razumovsky

Posted on

Understanding 'this' in JavaScript: Execution Context Explained

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" ✗
Enter fullscreen mode Exit fullscreen mode

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 this points 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:

  1. new binding (constructor calls)
  2. Explicit binding (call, apply, bind)
  3. Implicit binding (method calls)
  4. 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)
Enter fullscreen mode Exit fullscreen mode

What happens internally:

  1. JavaScript creates an Execution Context for showThis
  2. Looks at the call-site: showThis() - no object, no new, no explicit binding
  3. Applies default binding: ThisBinding = globalObject

In strict mode:

'use strict';

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

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

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

What happens internally:

  1. JavaScript evaluates user.greet - finds the function
  2. Creates an Execution Context for that function
  3. Looks at the call-site: user.greet() - called on user object
  4. 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen 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!
}
Enter fullscreen mode Exit fullscreen mode

The function is called without an object, so default binding applies.

Solutions:

1. Arrow function wrapper:

setTimeout(() => user.greet(), 1000); // "Alice"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

What happens with call():

  1. call invokes the function immediately
  2. First argument (user) becomes this
  3. 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
};
Enter fullscreen mode Exit fullscreen mode

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"!
Enter fullscreen mode Exit fullscreen mode

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]
    );
  };
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

What happens when you use new:

  1. Create a new empty object: {}
  2. Set the object's [[Prototype]] to User.prototype
  3. Execute User with this bound to the new object
  4. If User doesn'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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Precedence: When Rules Conflict

When multiple rules apply, here's the priority (highest to lowest):

  1. Arrow function - always uses lexical this, can't be overridden
  2. new - creates new object and binds to it
  3. Explicit binding (call/apply/bind) - you explicitly set this
  4. Implicit binding - method call on an object
  5. 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)
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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)
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Solution 2: Bind in constructor:

class User {
  constructor(name) {
    this.name = name;
    this.greet = this.greet.bind(this);
  }

  greet() {
    console.log('Hello, ' + this.name);
  }
}
Enter fullscreen mode Exit fullscreen mode

Solution 3: Class field (modern JavaScript):

class User {
  name;

  greet = () => {
    console.log('Hello, ' + this.name);
  }

  constructor(name) {
    this.name = name;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Fix: Keep the object reference or use bind:

console.log(obj.getValue()); // 42
// or
const getValue = obj.getValue.bind(obj);
console.log(getValue()); // 42
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

Fix: Arrow function or bind:

double() {
  return this.values.map(v => v * this.multiplier);
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Arrow functions as methods

const obj = {
  value: 42,
  getValue: () => this.value // this is NOT obj!
};
Enter fullscreen mode Exit fullscreen mode

Fix: Use regular function for methods:

const obj = {
  value: 42,
  getValue() { return this.value; }
};
Enter fullscreen mode Exit fullscreen mode

How to Debug 'this'

When this isn't what you expect:

  1. Find the call-site - where is the function actually called?
  2. 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
  3. Apply the precedence rules

Add logging:

function test() {
  console.log('this is:', this);
  console.log('this.constructor.name:', this.constructor.name);
  // Your actual code
}
Enter fullscreen mode Exit fullscreen mode

Summary: 'this' is All About the Call-Site

Key takeaways:

Four binding rules (in precedence order):

  1. new binding: this = newly created object
  2. Explicit binding: this = what you passed to call/apply/bind
  3. Implicit binding: this = object before the dot
  4. Default binding: this = global object (or undefined in strict mode)

Arrow functions:

  • Don't have their own this
  • Inherit this from 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)