تخطٍّ إلى المحتوى
Clinford Jordano Valencia
كل المقالات

JavaScript

JavaScript’s this Keyword, Four Rules and One Exception

this depends on how a function is called, not where it is written. Four call-site rules, the arrow function exception and how to fix a lost this in callbacks.

  • قراءة في 9 دقيقة

هذه الصفحة غير متاحة بعد باللغة العربية، لذا يُعرض النص الأصلي بالإنجليزية.

In JavaScript, this is decided at the call site: the same function gets a different this depending on how it is called. Four rules cover every regular function, checked in a fixed order, and arrow functions are the one exception because they have no this of their own. This post covers the rules, a decision tree for debugging them, and the three places this usually gets lost.

Why It Still Matters

With function components and hooks, many React developers rarely write this any more. It hasn’t gone away. You still meet it in:

  • class-based SDKs and API clients whose methods get passed around as callbacks
  • DOM event listeners
  • Web Components (every custom element is a class)
  • Node.js event emitters
  • most frontend interviews

When it breaks, the error shows up far from the cause: TypeError: Cannot read properties of undefined (reading 'queue'), thrown inside a timer or an array method, with nothing in the stack trace pointing at the line that dropped this. this is one part of what JavaScript sets up for every call; execution contexts and closures covers the rest.

The Four Rules, in Order

Ask these questions about the call, top to bottom. The first one that matches decides this.

RuleThe call looks likethis is
1. newnew Tracker()the newly created object
2. Explicitfn.call(obj), fn.apply(obj), bound()obj, or the value given to bind
3. Methodobj.fn()obj, the object before the dot
4. Plainfn()undefined in strict mode, else globalThis
Is it an arrow function?this of the surrounding codeyesnoCalled with new?the new objectyesnoCalled with call, apply or bind?the first argumentyesnoCalled as obj.method()?objyesnoundefined (strict) or globalThis
Check the call from the top. Arrow functions leave the tree at the first question.

1. Called With new

new does three things, and class constructors work the same way:

  1. creates an empty object whose prototype is the constructor’s prototype
  2. calls the function with this set to that object
  3. returns the object
function Tracker(name) {
  this.name = name;
  this.queue = [];
}
 
const checkout = new Tracker('checkout');
checkout.name; // 'checkout'

new even beats bind. Calling a bound function with new ignores the bound this (the bound arguments still apply):

const Bound = Tracker.bind({ name: 'ignored' });
new Bound('search').name; // 'search'

2. Called With call, apply or bind

call and apply run the function once with the this you pass. bind returns a new function with this fixed for good: calling it with call, or binding it again, does not change it.

function describe(prefix) {
  return `${prefix} ${this.name}`;
}
const user = { name: 'Ada' };
 
describe.call(user, 'Hi'); // 'Hi Ada'
describe.apply(user, ['Hi']); // 'Hi Ada'
 
const greet = describe.bind(user);
greet('Hello'); // 'Hello Ada'
greet.call({ name: 'Bob' }, 'Hey'); // 'Hey Ada': a bound function keeps its this

In sloppy (non-strict) code, passing null or undefined makes this the global object, and primitives such as 42 are wrapped in objects. Strict code passes the value through unchanged.

3. Called as a Method

When the call is obj.fn(), this is the object before the dot at the moment of the call. It is not the object the function was written in:

const counter = {
  count: 0,
  increment() {
    this.count += 1;
    return this.count;
  },
};
counter.increment(); // 1
 
const other = { count: 10, increment: counter.increment };
other.increment(); // 11: same function, different object before the dot

This is also why class methods work: they live on the prototype, but you call them as instance.method(), so this is the instance. Optional chaining (counter?.increment()) and brackets ((counter.increment)()) still count as method calls.

4. A Plain Call

Nothing matched, so it is a plain call: fn(). In strict mode this is undefined. ES modules and class bodies are always strict, so in modern code this is what you get:

function whoAmI() {
  return this;
}
whoAmI(); // undefined in a module or class; globalThis in a sloppy script

In an old-style script without 'use strict', this becomes globalThis (window in browsers), so this.count = 1 silently creates a global variable. The strict-mode TypeError is the better failure: it is loud and it happens at the right place.

One Function, Four Calls

The rules are easiest to remember as one picture: the same function, called four ways, gets four different values of this. Nothing about the function changed; only the call did.

function show()user.show()show()show.call(admin)new show()this = userthis = undefinedthis = adminthis = a new object
this belongs to the call, not the function. The same show function gets a different this from each call site.

The Exception: Arrow Functions

An arrow function has no this of its own. It uses the this of the code it was written in, and call, apply and bind can’t change that. Arrow functions also can’t be called with new (TypeError: arrow is not a constructor).

That makes them the right tool for callbacks inside methods:

tracker.ts
class Tracker {
  queue: string[] = [];
 
  start() {
    // The arrow function uses start()’s this: the instance.
    setInterval(() => this.flush(), 3000);
  }
 
  flush() {
    // send this.queue …
  }
}

And the wrong tool for object methods:

// In an ES module, where top-level this is undefined
const user = {
  name: 'Ada',
  greet: () => this.name, // this is the module’s this, not user
};
user.greet(); // TypeError: Cannot read properties of undefined (reading 'name')

Is That Everything? this Outside a Function Call

The four rules and the arrow exception cover every call of a function. this also has a value in a few places that aren’t function calls:

Wherethis is
Top level of an ES moduleundefined
Top level of a classic browser scriptThe global object (window)
Top level of a CommonJS module (Node.js)module.exports
A class field initializer (count = this.start)The instance being created
A static { … } block, or a static field initializerThe class itself
Direct eval('this')The this of the code around it

So the full answer is: the call decides this for regular functions, the surrounding code decides it for arrow functions, and the table above covers code that runs outside any call. When you need the global object, use globalThis rather than a top-level this.

Where This Gets Lost

Passing a Method as a Callback

This is the most common bug, and it follows directly from rules 3 and 4:

class Tracker {
  queue = [];
  track(event) {
    this.queue.push(event);
  }
}
 
const tracker = new Tracker();
['click', 'scroll'].forEach(tracker.track);
// TypeError: Cannot read properties of undefined (reading 'queue')
  1. tracker.track is read as a value: the function alone, without tracker
  2. forEach keeps it as a plain callback
  3. forEach calls it with no object before the dot
  4. The class body is strict, so this is undefined

Any of these fixes it:

['click', 'scroll'].forEach((event) => tracker.track(event)); // a method call again
['click', 'scroll'].forEach(tracker.track, tracker); // array methods accept a thisArg
['click', 'scroll'].forEach(tracker.track.bind(tracker)); // fixed for good

Timers have the same problem: setTimeout(tracker.flush, 1000) calls flush as a plain function.

Event Listeners

A regular function passed to addEventListener is called with this set to the element the listener is attached to. An arrow function keeps the surrounding this instead. Reading event.currentTarget is clearer than either:

button.addEventListener('click', function (event) {
  this === event.currentTarget; // true: the button
});

Destructuring and Indirect Calls

Taking a method off its object loses this too, however you do it:

const { track } = tracker;
track('click'); // TypeError: no object before the dot
 
(0, tracker.track)('click'); // TypeError: the comma operator returns the bare function

Bundlers emit (0, fn)() on purpose, to call imported functions without a this.

Where You Meet It Day to Day

The same rules, in the places they usually bite.

A Handler in a React Class Component

Class components are still common in older codebases and error boundaries. React calls your handler as a plain function, so a regular method loses the instance:

class SaveButton extends React.Component {
  handleClick() {
    this.props.onSave(); // TypeError: this is undefined
  }
  render() {
    return <button onClick={this.handleClick}>Save</button>;
  }
}

The usual fix is an arrow class field, handleClick = () => { this.props.onSave(); };, so this is fixed to the instance when the object is created.

A Method Passed to then

Service classes often hand a method to a promise chain. then calls it as a plain function:

class UserService {
  #cache = new Map();
 
  remember(user) {
    this.#cache.set(user.id, user);
    return user;
  }
 
  load(id) {
    return fetch(`/api/users/${id}`)
      .then((response) => response.json())
      .then(this.remember); // TypeError when the response arrives
  }
}

The error appears later, inside a promise, with nothing in the stack trace pointing at load. Write .then((user) => this.remember(user)).

Route Handlers in Node.js

The same thing happens when a framework stores your method and calls it later:

class OrdersController {
  constructor(repository) {
    this.repository = repository;
  }
  async list(request, response) {
    response.json(await this.repository.all());
  }
}
 
const orders = new OrdersController(repository);
app.get('/orders', orders.list); // TypeError on every request
app.get('/orders', (request, response) => orders.list(request, response)); // works

Arrow Functions in an Options Object

Frameworks with an “options object” style, such as Vue’s Options API, call your methods with this set to the component. An arrow function can’t receive that this:

export default {
  data() {
    return { count: 0 };
  },
  methods: {
    increment() {
      this.count++; // this is the component
    },
    reset: () => {
      this.count = 0; // this is the module’s this (undefined), not the component
    },
  },
};

Event Emitters in Node.js

Node.js calls a regular listener function with this set to the emitter, and an arrow function keeps its surrounding this:

import { EventEmitter } from 'node:events';
 
const emitter = new EventEmitter();
emitter.on('ready', function () {
  console.log(this === emitter); // true
});
emitter.on('ready', () => {
  console.log(this === emitter); // false: the module’s this
});
emitter.emit('ready');

As with DOM listeners, reading the object you need from a variable or the event is clearer than relying on this.

Going Deeper: Classes, Getters and Types

Derived Constructors Must Call super() First

In a class that extends another, this doesn’t exist until the parent constructor has created it:

class Base {}
class Child extends Base {
  constructor() {
    this.x = 1; // too early
    super();
  }
}
new Child();
// ReferenceError: Must call super constructor in derived class before accessing 'this'

The parent class builds the object, so the child can only touch it after super() returns. Class fields declared in the child are added at that moment too, which is why they can’t be read inside the parent’s constructor.

Static Methods and Getters Follow the Same Rules

  • In a static method, this is whatever is before the dot, usually the class itself. Called through a subclass, Sub.create(), it is the subclass, which lets static factory methods create the right type. Passed around as a callback, it loses this like any other method.
  • A getter or setter runs with this set to the object the property was read from, even when the getter is inherited. If admin inherits an upper getter from user, admin.upper uses admin’s data.

Let TypeScript Check this for You

TypeScript can declare what this must be with a fake first parameter, and it reports calls that would lose it:

function onClick(this: HTMLButtonElement, event: MouseEvent) {
  this.disabled = true;
}
 
button.addEventListener('click', onClick); // fine: the listener’s this is the button
onClick(new MouseEvent('click'));
// error TS2684: The 'this' context of type 'void' is not assignable to method's 'this'

With noImplicitThis (part of strict), TypeScript also flags any use of this whose type it can’t work out, which catches most “lost this” bugs before they reach a browser.

Which Fix to Choose

FixUse it whenWhat it costs
Arrow function at the call siteA one-off callbackNothing worth measuring; the intent is visible where the call happens
bind once and keep the referenceYou need to remove the listener laterA little setup code
Arrow class field (track = (e) => {…})A public API whose methods are passed aroundOne copy per instance instead of one on the prototype; subclasses can’t call super.track()
thisArgArray methods such as forEach, map, filterOnly some APIs accept one, and readers often miss it
No this at all (closures and factories)Small modules where you control the designA different style; no shared prototype

Test Yourself

Each snippet runs in an ES module (strict mode). Predict the output, then open the answer. Every answer was checked by running the code.

التحدي 1

The Same Function, Two Calls

const user = {
  name: 'Ada',
  greet() {
    return `Hi ${this?.name}`;
  },
};
const greet = user.greet;
console.log(user.greet(), '|', greet());
إظهار الإجابة

Hi Ada | Hi undefined

  • user.greet() is a method call, so this is user.
  • greet() is a plain call, so this is undefined and this?.name is undefined.
  • Without the ?., the second call would throw.

التحدي 2

Function vs Arrow in a Timer

const counter = {
  start() {
    setTimeout(function () {
      console.log('function:', this === counter);
    }, 0);
    setTimeout(() => {
      console.log('arrow:', this === counter);
    }, 0);
  },
};
counter.start();
إظهار الإجابة

function: false, then arrow: true

  • The timer calls the regular function without counter (browsers pass window, Node.js passes its Timeout object).
  • The arrow function uses the this of start(), which was called as counter.start().

التحدي 3

Can You Rebind a Bound Function?

function label() {
  return this.name;
}
const a = { name: 'a', label };
const b = { name: 'b', label: label.bind({ name: 'bound' }) };
 
console.log(a.label(), b.label(), b.label.call({ name: 'call' }));
إظهار الإجابة

a bound bound

  • bind fixes this for good: neither a method call (b.label()) nor call can change it.
  • Only new overrides a bound this.

التحدي 4

The Callback Inside a Method

const cart = {
  items: [5, 10],
  total: 0,
  sum() {
    this.items.forEach(function (price) {
      this.total += price;
    });
    return this.total;
  },
};
console.log(cart.sum());
إظهار الإجابة

TypeError: Cannot read properties of undefined (reading 'total')

  • this.items works, because sum is called as a method.
  • forEach calls the inner regular function as a plain call, so its this is undefined.
  • Fix: an arrow function, (price) => { this.total += price; }, which prints 15.

التحدي 5

An Arrow Class Field Passed Around

class Stopwatch {
  seconds = 0;
  tick = () => {
    this.seconds += 1;
  };
}
const watch = new Stopwatch();
const { tick } = watch;
tick();
tick();
console.log(watch.seconds);
إظهار الإجابة

2

  • An arrow class field is created per instance and captures that instance as this.
  • So destructuring it off the object doesn’t lose anything.

التحدي 6

A Function in an Array

const callbacks = [
  function () {
    return this.length;
  },
];
console.log(callbacks[0]());
إظهار الإجابة

1

  • callbacks[0]() is a method call on the array: bracket access counts, just like a dot.
  • So this is the array, and its length is 1.

Key Takeaways

  • this is set by how a function is called, not by where it is written.
  • Check the call in order: new, then call/apply/bind, then obj.method(), then a plain call.
  • Arrow functions have no this. They use the surrounding one, and nothing can rebind it.
  • Passing obj.method passes the function without obj. Wrap it, bind it or make it an arrow field.
  • In modules and classes a plain call gives undefined, which fails loudly and early. That is a feature.

Methods find their functions through the prototype chain, which is covered in what happens when you read a property. Practise the rules in the JavaScript round of “What does this print?”.

Sources

الوسوم

الأسئلة الشائعة

Why is this undefined inside my callback?

Passing obj.method hands over the function alone, without obj. The code that receives it calls it as a plain function, and in strict mode (every ES module and class body) a plain call gives undefined. Wrap it in an arrow function, bind it, or define the method as an arrow class field.

Should I use arrow functions everywhere?

NoArrow functions are the right choice for callbacks inside methods, because they keep the method’s this. They are the wrong choice for object methods that need the object, for constructors (an arrow function can’t be called with new) and for event handlers that rely on this being the element.

What is this at the top level of a file?

In an ES module it is undefined. In a classic browser script it is the global object, window. In a CommonJS module in Node.js it is module.exports. If you need the global object, use globalThis, which works everywhere.

What does bind return?

A new function with this (and optionally some leading arguments) fixed permanently. The original function is unchanged, and each call to bind creates a different function, which matters when removing event listeners.

Why can’t I use this before super() in a subclass constructor?

In a derived class the parent constructor creates the object. Until super() returns there is no this to use, so JavaScript throws a ReferenceError if you touch it.

How can TypeScript help with this?

Declare the expected type as a first this parameter, for example function handler(this: HTMLButtonElement), and enable strict mode (noImplicitThis). TypeScript then reports calls that would lose or mismatch this.

Why don’t React function components use this?

They are plain functions called by React, and their state lives in hooks rather than on an instance. Data flows through props, state and closures, so there is no this to lose.

What is globalThis?

A standard name for the global object in every environment: window in browsers, global in Node.js, self in workers. Use it instead of relying on this at the top level, which differs between scripts and modules.