Why this Changes in JavaScript Event Handlers and Methods

Hero image for Why this Changes in JavaScript Event Handlers and Methods. Image by Jerry Wang.
Hero image for 'Why this Changes in JavaScript Event Handlers and Methods.' Image by Jerry Wang.

This Article is Over Ten Years Old...

Things can and do move very quickly in tech, which means that tech-related articles go out of date almost as soon as they have been written and published. If you are looking for up-to-date technical advice or opinion, it is unlikely that you will find it on this page.

You may find that my recent articles are more relevant, and you are always welcome to drop me a line if you have a specific technical problem you are trying to solve.

The JavaScript this keyword has been confusing people for years for one very ordinary reason: it looks like it should point to the object a function "belongs" to, but that is not actually how it works.

That assumption is where the trouble starts. New developers see a function living on an object and quite reasonably think this inside that function will always mean that object. Sometimes it does. Sometimes it does not. The deciding factor is not where the function was written. It is how the function is called.

That is the rule that makes this feel slippery until it clicks.


this is set by the call site

The shortest accurate rule is this: this is determined by the way a function is invoked.

Look at a simple method call:

const cart = {  total: 99,  printTotal(): void {    console.log(this.total);  },};cart.printTotal();

Here, this inside printTotal points to cart because the function is being called as a method of cart.

That feels natural enough. The trouble appears when the same function is called differently.

const print = cart.printTotal;print();

Now, the function has been detached from the object. It is no longer being invoked as cart.printTotal(). It is being called as a plain function, so the value of this changes.

That is why this bugs often show up when a method is passed around as a callback rather than called directly where it lives.


Methods Work until You Pass Them Elsewhere

This is the classic trap.

const timer = {  seconds: 0,  tick(): void {    this.seconds += 1;  },};setTimeout(timer.tick, 1000);

Many beginners expect that to increment timer.seconds. It does not, because setTimeout is not calling the function as timer.tick(). It is calling the function later as a callback. The original method context has been lost.

That is one of the most useful ways to think about this: it is fragile when functions are passed around.


Event Handlers Add Another Layer of Confusion

DOM event handlers make this feel even stranger because there are two very tempting candidates:

  • the object where you wrote the handler
  • the element that fired the event

In a normal DOM event listener, this inside a regular function callback refers to the element the listener is attached to.

const button = document.querySelector<HTMLButtonElement>('.save-button');button?.addEventListener('click', function () {  console.log(this.textContent);});

In that example, this refers to the button element.

That can be useful, but it also means this inside an event listener is not some magical reference back to the module, component, or object where you defined the handler. It is tied to the way the browser invokes that function.

If you want the element more explicitly, event.currentTarget is often clearer:

button?.addEventListener('click', (event) => {  const currentTarget = event.currentTarget as HTMLButtonElement;  console.log(currentTarget.textContent);});

That tends to be easier to read than relying on this, especially once callbacks become more nested.


Arrow Functions Change the Picture

Arrow functions do not have their own this. Instead, they close over the surrounding this from the place where they were created.

That sounds abstract until you compare the behaviour directly.

const cart = {  total: 99,  printLater(): void {    setTimeout(() => {      console.log(this.total);    }, 1000);  },};

Because the arrow function does not create a new this, it uses the this from printLater, which in this case is cart.

That makes arrow functions a practical fix for many callbackrelated this bugs. The tradeoff is that they do not behave like regular functions, which is sometimes exactly what you want and sometimes not.

The important part is not to think "arrow functions make this easier" in some vague way. The precise reason they help is that they do not rebind this at the callback boundary.


bind exists for exactly this problem

Another common fix is bind.

const timer = {  seconds: 0,  tick(): void {    this.seconds += 1;  },};setTimeout(timer.tick.bind(timer), 1000);

bind creates a new function whose this is permanently set to the value you provide.

That is useful when:

  • you want to pass a method as a callback
  • you need a stable function reference with a fixed context
  • an API is going to call your function later in a way you do not control

If arrow functions are the "inherit the surrounding this" solution, bind is the "pin this to exactly this object" solution.


The bug is usually not this itself

Most of the time the real bug is not that JavaScript changed the rules halfway through. The rules were consistent. Our mental model was wrong.

If you read code like this:

const player = {  name: 'Ellie',  announce(): void {    console.log(this.name);  },};const handler = player.announce;handler();

the important question is not "why did JavaScript betray me?" It is "how is handler being called?" Once the answer is "as a plain function", the changed value of this stops feeling random.


In event code, this is often less clear than the event object

This is not a moral rule, but it is a useful one.

When you are handling browser events, code is often easier to read if you work with the event object rather than relying on this.

For example:

button?.addEventListener('click', (event) => {  const target = event.currentTarget as HTMLButtonElement;  target.disabled = true;});

That says more plainly what is happening. You are taking the current target of the event and updating it. There is no extra context puzzle for the next developer to mentally resolve.

That is especially helpful when the surrounding code already uses objects, classes, or closures that all have their own possible meanings for this.


Sometimes the best fix is to avoid this

This is the part that newer developers often do not hear early enough.

You do not always need this at all.

If a function can take explicit arguments instead of depending on callsite context, that is often easier to test and easier to reason about.

const printTotal = (total: number): void => {  console.log(total);};printTotal(99);

That is not always the right design, but it is worth considering. Many this problems disappear once the code stops depending on implicit context and starts using explicit data flow instead.


Wrapping up

this changes in JavaScript because it is determined by how a function is called, not by where that function was originally written. Methods, detached callbacks, event listeners, arrow functions, and bind all change the calling context in different ways, which is why the behaviour can feel inconsistent until you know the rule underneath it.

Key Takeaways

  • this is set by the call site, not by where the function lives.
  • Methods lose their original context easily when passed around as callbacks.
  • In DOM event listeners, regular function callbacks usually get this bound to the current element.
  • Arrow functions do not create their own this; they inherit it from the surrounding scope.
  • bind is the direct way to lock a function to a specific this value.

Once you stop thinking of this as ownership and start thinking of it as callsite context, a lot of awkward JavaScript suddenly starts making much more sense.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript