How the `this` Keyword Works in JavaScript: Regular Functions, Arrow Functions, and Methods

In JavaScript, the value of this is determined by how a function is invoked rather than where it is defined, following five binding rules that prioritize new binding, explicit binding, implicit binding, and default binding, while arrow functions capture this lexically from their surrounding scope.

Understanding how the this keyword works in JavaScript is essential for mastering object-oriented patterns and avoiding common context-related bugs. The leonardomso/33-js-concepts repository provides an exhaustive test suite in tests/object-oriented/this-call-apply-bind/this-call-apply-bind.test.js that demonstrates these behaviors through executable specifications. This guide explains the five binding rules that govern this across different invocation contexts based on the actual source code implementation.

The Five Binding Rules for this

JavaScript determines the value of this through five distinct binding rules, each triggered by a specific invocation pattern. The test suite in this-call-apply-bind.test.js validates these rules with test cases such as "should have different this values depending on how function is called" and "Rule 5: Arrow Functions (Lexical this)".

Default Binding in Standalone Functions

When a regular function is invoked without any context—simply as a standalone function—default binding applies. In strict mode, this is set to undefined; in non-strict mode, it defaults to the global object.

function showThis() { 
  return this; 
}

// In strict mode
expect(showThis()).toBeUndefined();

Implicit Binding in Method Calls

When a function is called as a property of an object using dot notation (obj.method()), implicit binding sets this to the object that appears immediately before the dot. This rule applies to regular functions assigned as object properties.

const obj = { 
  name: 'Object',
  showThis: function() { return this; }
};

expect(obj.showThis()).toBe(obj);

Explicit Binding with call, apply, and bind

JavaScript provides three methods for manually specifying the this context: explicit binding. The call() method invokes the function immediately with a given this value and arguments passed individually, while apply() accepts arguments as an array. The bind() method returns a new function with this permanently fixed, ignoring subsequent attempts to rebind.

function greet() { 
  return `Hello, ${this.name}!`; 
}

const alice = { name: 'Alice' };
expect(greet.call(alice)).toBe('Hello, Alice!');

function introduce(greeting, punctuation) {
  return `${greeting}, I'm ${this.name}${punctuation}`;
}

const bob = { name: 'Bob' };
expect(introduce.apply(bob, ['Hi', '!'])).toBe('Hi, I\'m Bob!');

function getName() { 
  return this.name; 
}

const boundGetName = getName.bind(alice);
expect(boundGetName()).toBe('Alice');
expect(boundGetName.call({ name: 'Bob' })).toBe('Alice'); // call ignored

new Binding in Constructors

When a function is invoked with the new keyword, JavaScript creates a fresh object and binds this to that new instance. This new binding takes precedence over all other binding types and allows constructors to initialize instance properties.

class Rectangle {
  constructor(w, h) { 
    this.width = w; 
    this.height = h; 
  }
  getArea() { 
    return this.width * this.height; 
  }
}

const rect = new Rectangle(10, 5);
expect(rect.getArea()).toBe(50);

Lexical Binding in Arrow Functions

Arrow functions do not have their own this binding. Instead, they capture the this value from their enclosing lexical scope at definition time. This makes arrow functions immune to rebinding via call, apply, or bind, and ideal for preserving context in callbacks.

const obj = {
  name: 'Object',
  method() {
    const arrow = () => this.name;
    return arrow();
  }
};

expect(obj.method()).toBe('Object');

Arrow functions ignore explicit binding attempts:

const obj = {
  name: 'Original',
  getArrow() { 
    return () => this.name; 
  }
};

const arrow = obj.getArrow();
expect(arrow.call({ name: 'Other' })).toBe('Original');

Binding Priority and Rule Precedence

The five binding rules follow a strict hierarchy of precedence: new binding > explicit binding > implicit binding > default binding. Arrow functions operate outside this hierarchy entirely, as they close over the lexical this from their creation context rather than participating in runtime binding determination.

How this Works in Different Function Types

Regular Functions and Dynamic Context

Regular functions utilize dynamic binding, meaning the value of this depends entirely on the invocation pattern at runtime. The same function can reference different objects as this when called as a method versus a standalone function, as demonstrated in the test "should have different this values depending on how function is called".

Arrow Functions and Lexical Scope

Arrow functions inherit this from their enclosing scope, making them predictably bound to their creation context. This behavior is particularly valuable for callback functions where preserving the outer method's context is necessary.

class Counter {
  constructor() { 
    this.count = 0; 
  }
  
  incrementWithArrow() {
    [1, 2, 3].forEach(() => { 
      this.count++; 
    });
  }
}

const c = new Counter();
c.incrementWithArrow();
expect(c.count).toBe(3);

Source Code Reference

The implementation details and test cases for these binding rules are located in tests/object-oriented/this-call-apply-bind/this-call-apply-bind.test.js within the leonardomso/33-js-concepts repository. This file contains exhaustive unit tests covering edge cases for all five binding rules, arrow function behavior, and interaction between different invocation patterns. The index.js entry point exposes these modules as part of the package build, ensuring the examples are validated against the library's public API.

Summary

  • Invocation determines binding: The value of this depends on how a function is called, not where it is defined.
  • Five binding rules govern this: Default, implicit, explicit, new, and lexical binding each apply to specific invocation patterns.
  • Arrow functions capture lexical this: They inherit this from their surrounding scope and ignore attempts to rebind with call, apply, or bind.
  • Binding precedence matters: new binding overrides explicit binding, which overrides implicit binding, which overrides default binding.
  • Test suite validation: The 33-js-concepts repository provides executable proof of these behaviors in this-call-apply-bind.test.js.

Frequently Asked Questions

What is the difference between this in arrow functions and regular functions?

Regular functions use dynamic binding where this is determined by how the function is invoked at runtime, while arrow functions use lexical binding where this is captured from the surrounding scope at definition time. Arrow functions cannot be rebound using call, apply, or bind, making them ideal for callbacks where the outer context must be preserved.

How does the new keyword affect this binding?

When a function is invoked with new, JavaScript creates a new empty object, sets this to reference that object within the constructor, and returns the object implicitly unless the constructor returns a different object explicitly. This new binding has the highest priority and overrides both explicit and implicit binding rules.

Can you change this inside an arrow function using call or bind?

No, arrow functions do not have their own this binding and therefore ignore attempts to set this via call, apply, or bind. According to the test cases in this-call-apply-bind.test.js, calling an arrow function with call({ name: 'Other' }) will still return the lexical this value captured at creation time, not the object passed to call.

Why is this undefined in my JavaScript function?

If this is undefined in a standalone function call, your code is likely running in strict mode where default binding sets this to undefined rather than the global object. This is the expected behavior for default binding in modern JavaScript and helps prevent accidental global variable pollution.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →