JavaScriptintermediate

this Keyword Deep Dive — The Complete Guide

Master the JavaScript this keyword: global context, function binding, arrow functions, call/apply/bind, and common pitfalls. Stop guessing what this refers to.

12 min read·Published Mar 8, 2026
thiscontextbindingjavascript

Why Is this So Confusing?

In most languages, this always refers to the instance of the class where the method is defined. In JavaScript, this is determined by how a function is called, not where it is written. The same function can have different this values depending on the call site.

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

const alice = { name: 'Alice', greet };
const bob = { name: 'Bob', greet };

alice.greet(); // Hello, Alice
bob.greet();   // Hello, Bob
greet();       // Hello, undefined (or error in strict mode)

Same function, three different this values. Once you understand the rules, the confusion disappears entirely.

The Four Binding Rules

JavaScript determines this using four rules, applied in this priority order:

Priority   Rule                When It Applies
────────────────────────────────────────────────────────────
   1       new binding         Function called with new keyword
   2       Explicit binding    call(), apply(), or bind()
   3       Implicit binding    Function called as object method
   4       Default binding     Standalone function call

Let's examine each one in detail.

Rule 1: Default Binding

When a function is called as a standalone function (not as a method, not with new, not with call/apply/bind), this falls back to the default.

In Non-Strict Mode

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

showThis(); // Window object (browser) or global object (Node.js)

In Strict Mode

'use strict';

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

showThis(); // undefined

globalThis (ES2020)

The globalThis property provides a standard way to access the global this value across environments.

// Works everywhere: browser, Node.js, Web Workers
console.log(globalThis);

// Browser: globalThis === window
// Node.js: globalThis === global
// Worker:  globalThis === self

Node.js Module Context

In Node.js, the top-level this in a module is not the global object — it is module.exports.

// In a Node.js file (CommonJS)
console.log(this === module.exports); // true
console.log(this === global);        // false

// But inside a function
function test() {
  console.log(this === global); // true (non-strict)
}
test();

Rule 2: Implicit Binding

When a function is called as a method of an object (using dot notation), 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

Nested Objects

Only the immediate (last) object in the chain matters.

const company = {
  name: 'Acme Corp',
  department: {
    name: 'Engineering',
    getName() {
      return this.name;
    }
  }
};

console.log(company.department.getName()); // 'Engineering' (not 'Acme Corp')

Implicit Binding Loss

This is the most common source of this bugs. When you extract a method from an object, the binding is lost.

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

// Works fine — implicit binding
user.greet(); // Hi, I'm Alice

// Broken — method reference loses binding
const greetFn = user.greet;
greetFn(); // Hi, I'm undefined

// Also broken — passing as callback
setTimeout(user.greet, 100); // Hi, I'm undefined

Here is what happens under the hood:

user.greet()          greetFn()
     |                     |
     v                     v
this = user           this = global/undefined
     |                     |
     v                     v
"Hi, I'm Alice"       "Hi, I'm undefined"

When you assign user.greet to greetFn, you get a plain function reference. The connection to user is gone. The function no longer "knows" it came from user.

Rule 3: Explicit Binding — call, apply, bind

You can force this to any value using call(), apply(), or bind().

call()

Calls the function immediately with a specified this and individual arguments.

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

const alice = { name: 'Alice' };
const bob = { name: 'Bob' };

introduce.call(alice, 'Hello', '!');   // Hello, I'm Alice!
introduce.call(bob, 'Hey', '.');       // Hey, I'm Bob.

apply()

Same as call(), but takes arguments as an array.

introduce.apply(alice, ['Hello', '!']); // Hello, I'm Alice!
introduce.apply(bob, ['Hey', '.']);      // Hey, I'm Bob.

call vs apply — When to Use Which

Method    Arguments      Use When
──────────────────────────────────────────────────
call()    individual     you know the args at write time
apply()   array          args are in an array already
// Practical: borrowing array methods
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };

// Use call with Array.prototype methods
const arr = Array.prototype.slice.call(arrayLike);
console.log(arr); // ['a', 'b', 'c']

// apply is handy with Math.max
const numbers = [5, 2, 8, 1, 9];
Math.max.apply(null, numbers); // 9

// Modern alternative: spread
Math.max(...numbers); // 9

bind()

Returns a new function with this permanently set. Does not call the function immediately.

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

// Create a bound version
const boundGreet = user.greet.bind(user);

// Now it works everywhere
boundGreet();                // Hi, I'm Alice
setTimeout(boundGreet, 100); // Hi, I'm Alice

// bind can also pre-fill arguments (partial application)
function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5));  // 10
console.log(double(10)); // 20

bind Is Permanent

Once bound, this cannot be overridden — not even by call or apply.

const user = { name: 'Alice' };
const other = { name: 'Bob' };

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

const bound = greet.bind(user);
bound();                    // Alice
bound.call(other);          // Alice (still Alice, bind wins)
bound.apply(other);         // Alice (still Alice)

Rule 4: new Binding

When a function is called with new, JavaScript creates a new object and sets this to that object.

function Person(name, age) {
  // this = {} (new empty object, created automatically)
  this.name = name;
  this.age = age;
  // return this (implicit)
}

const alice = new Person('Alice', 30);
console.log(alice.name); // Alice
console.log(alice.age);  // 30

The new keyword does four things:

Step 1:  Create a new empty object             {}
Step 2:  Link its [[Prototype]] to             Person.prototype
Step 3:  Set this = the new object             this = {}
Step 4:  Return the object (unless             return this
         function explicitly returns
         a different object)
// Explicit return of object overrides new
function Weird() {
  this.name = 'original';
  return { name: 'override' }; // returning an object replaces this
}

const w = new Weird();
console.log(w.name); // 'override'

// But returning a primitive does NOT override
function Normal() {
  this.name = 'original';
  return 42; // primitives are ignored
}

const n = new Normal();
console.log(n.name); // 'original'

Arrow Functions — Lexical this

Arrow functions do not have their own this. They inherit this from the enclosing lexical scope — the scope where the arrow function was defined, not where it is called.

const user = {
  name: 'Alice',
  // Regular function — this = user (implicit binding)
  greetRegular() {
    console.log(`Regular: ${this.name}`);
  },
  // Arrow function — this = enclosing scope (NOT user)
  greetArrow: () => {
    console.log(`Arrow: ${this.name}`);
  }
};

user.greetRegular(); // Regular: Alice
user.greetArrow();   // Arrow: undefined (this = global/module scope)

Where Arrow Functions Shine

Arrow functions solve the callback this problem permanently.

// Problem: regular function in callback loses this
const timer = {
  seconds: 0,
  start() {
    setInterval(function () {
      this.seconds++; // BUG: this = global, not timer
      console.log(this.seconds);
    }, 1000);
  }
};

// Fix 1: Arrow function (best)
const timer = {
  seconds: 0,
  start() {
    setInterval(() => {
      this.seconds++; // this = timer (lexical binding from start())
      console.log(this.seconds);
    }, 1000);
  }
};

// Fix 2: const self = this (old pattern)
const timer = {
  seconds: 0,
  start() {
    const self = this;
    setInterval(function () {
      self.seconds++;
      console.log(self.seconds);
    }, 1000);
  }
};

// Fix 3: bind (also works)
const timer = {
  seconds: 0,
  start() {
    setInterval(function () {
      this.seconds++;
      console.log(this.seconds);
    }.bind(this), 1000);
  }
};

Arrow Functions Cannot Be Rebound

call, apply, and bind have no effect on arrow functions' this.

const arrow = () => {
  console.log(this);
};

const obj = { name: 'test' };

arrow.call(obj);   // global/undefined (obj is ignored)
arrow.apply(obj);  // global/undefined
arrow.bind(obj)(); // global/undefined

When NOT to Use Arrow Functions

// 1. Object methods — this will not refer to the object
const obj = {
  value: 42,
  getValue: () => this.value // undefined, not 42
};

// 2. Prototype methods
function Person(name) {
  this.name = name;
}
Person.prototype.greet = () => {
  return this.name; // undefined, not the instance
};

// 3. Event handlers that need the element
button.addEventListener('click', () => {
  console.log(this); // Window, not the button
});

// Use regular function instead
button.addEventListener('click', function () {
  console.log(this); // the button element
});

// 4. Constructors — arrow functions cannot be used with new
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor

this in Classes

ES6 classes use this the same way as constructor functions, but with cleaner syntax.

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

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

const alice = new User('Alice');
alice.greet(); // Hi, I'm Alice

// But the same extraction problem exists
const greetFn = alice.greet;
greetFn(); // TypeError in strict mode (class bodies are strict by default)

Class Fields Fix the Extraction Problem

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

  // Class field with arrow function — this is always bound to the instance
  greet = () => {
    console.log(`Hi, I'm ${this.name}`);
  };
}

const alice = new User('Alice');
const greetFn = alice.greet;
greetFn(); // Hi, I'm Alice (works because arrow function captures this)

setTimeout(alice.greet, 100); // Hi, I'm Alice (works in callbacks too)

The tradeoff: each instance gets its own copy of the method (it is not on the prototype). For most apps this is fine, but it uses slightly more memory with thousands of instances.

this in Callbacks

Callbacks are the most common place where this goes wrong. Let's walk through every scenario.

Event Handlers

class App {
  constructor() {
    this.count = 0;
    this.button = document.querySelector('#btn');
  }

  // Option 1: bind in constructor
  init() {
    this.button.addEventListener('click', this.handleClick.bind(this));
  }

  // Option 2: arrow function wrapper
  init() {
    this.button.addEventListener('click', (e) => this.handleClick(e));
  }

  // Option 3: class field arrow (defined above)
  handleClick = () => {
    this.count++;
    console.log(`Clicked ${this.count} times`);
  };

  handleClick() {
    this.count++;
    console.log(`Clicked ${this.count} times`);
  }
}

Array Methods

const processor = {
  prefix: 'ITEM',
  process(items) {
    // Arrow function — this inherited from process()
    return items.map(item => `${this.prefix}: ${item}`);
  }
};

console.log(processor.process(['a', 'b', 'c']));
// ['ITEM: a', 'ITEM: b', 'ITEM: c']

// With regular function, this would be undefined/global
const broken = {
  prefix: 'ITEM',
  process(items) {
    return items.map(function (item) {
      return `${this.prefix}: ${item}`; // BUG: this is not broken
    });
  }
};

Promise Chains

class DataService {
  constructor() {
    this.baseUrl = 'https://api.example.com';
  }

  fetchUser(id) {
    // Arrow functions keep this throughout the chain
    return fetch(`${this.baseUrl}/users/${id}`)
      .then(res => res.json())
      .then(data => {
        console.log(`Fetched from ${this.baseUrl}`); // works
        return data;
      });
  }
}

The Complete Binding Decision Flowchart

When you see this in code, follow this decision tree:

Is it an arrow function?
├── YES → this = enclosing scope's this (lexical). Done.
└── NO → How is the function called?
    ├── new MyFunc()?
    │   └── this = newly created object
    ├── func.call(obj) / func.apply(obj) / func.bind(obj)?
    │   └── this = obj
    ├── obj.func()?
    │   └── this = obj
    └── func() (standalone)?
        ├── strict mode → this = undefined
        └── sloppy mode → this = globalThis

Common Mistakes and Fixes

Mistake 1: Extracting Methods

// BUG
class Logger {
  prefix = '[LOG]';
  log(msg) {
    console.log(`${this.prefix} ${msg}`);
  }
}

const logger = new Logger();
const { log } = logger; // destructured — loses this
log('test'); // TypeError: Cannot read properties of undefined

// FIX: Use arrow function class field
class Logger {
  prefix = '[LOG]';
  log = (msg) => {
    console.log(`${this.prefix} ${msg}`);
  };
}

Mistake 2: Nested Functions

const counter = {
  count: 0,
  increment() {
    // BUG: nested regular function
    function addOne() {
      this.count++; // this = global, not counter
    }
    addOne();
  }
};

// FIX: arrow function
const counter = {
  count: 0,
  increment() {
    const addOne = () => {
      this.count++; // this = counter (lexical)
    };
    addOne();
  }
};

Mistake 3: forEach with this

class TaskManager {
  tasks = [];

  addTasks(newTasks) {
    // BUG: regular function callback
    newTasks.forEach(function (task) {
      this.tasks.push(task); // this = undefined in strict mode
    });

    // FIX 1: arrow function
    newTasks.forEach((task) => {
      this.tasks.push(task); // works
    });

    // FIX 2: thisArg parameter (forEach, map, filter, etc.)
    newTasks.forEach(function (task) {
      this.tasks.push(task); // works
    }, this);
  }
}

Mistake 4: setTimeout and setInterval

class Slideshow {
  currentSlide = 0;

  start() {
    // BUG
    setInterval(function () {
      this.currentSlide++; // this = global
    }, 3000);

    // FIX
    setInterval(() => {
      this.currentSlide++; // this = Slideshow instance
    }, 3000);
  }
}

Determining this at a Glance — Quick Reference

Call Style                     this Value               Example
─────────────────────────────────────────────────────────────────────────
func()                         global / undefined       greet()
obj.func()                     obj                      user.greet()
func.call(obj)                 obj                      greet.call(user)
func.apply(obj)                obj                      greet.apply(user)
func.bind(obj)()               obj                      greet.bind(user)()
new Func()                     new instance             new User()
() => {}                       enclosing this           setTimeout(() => {})
class method                   instance (if called right)  user.greet()
event handler (regular)        DOM element              btn.onclick = fn
event handler (arrow)          enclosing this           btn.onclick = () => {}

Key Takeaways

  • this is determined at call time, not definition time (except for arrow functions)
  • Four rules in priority order: new > explicit (call/apply/bind) > implicit (dot notation) > default (standalone)
  • Arrow functions have lexical this — they inherit from the enclosing scope and cannot be rebound
  • bind() returns a new permanently-bound function; call() and apply() invoke immediately
  • The most common bug is implicit binding loss — extracting a method from an object strips its context
  • Use arrow function class fields to avoid binding issues in modern code
  • globalThis provides a cross-environment way to access the global object
  • When in doubt, follow the decision flowchart: arrow? -> new? -> explicit? -> implicit? -> default

Found this helpful?

Support devsofus — help us keep creating free dev guides.

Related Articles