`this` is the worst - JavaScript

For a lot of us, one of the toughest parts of JavaScript is this -- that is, figuring out what this is. The academic answer is that this is an "implicit extra parameter" that is automatically passed. You can access it, but you can't mutate it after it's set.

So, at any given point in the code, "What is this"? The answer is: it depends on a lot of things.

But let's start with the simplest case, code in the global scope.

Example 1: Global scope

console.log(`this in the global scope is ${this}`)
var b = 123;
console.log(`this.b is ${this.b}`);
// console results:
//   this in the global scope is [object Window]
//   this.b is 123

(Note: console results throughout this post reflect the output seen in Chrome)

When we're not inside a class or an object, this is what is called the "global object", which in a browser is the Window object.

(Notice that we used var here instead of let or const as provided by ES6. That's because a feature of let is that it intentionally prevents assigning variables to the global scope. Normally you want to avoid var and use let or const; this example is just to illustrate what this is in the global scope.)

Next up, let's talk about non-arrow functions, the kind where the function is defined like this:

function foo() {
  // function body goes here
}

For functions like these, this is the thing to which the function belongs, based on how you invoke the function. Let's look at some examples.

Example 2: Within a non-arrow function in the global scope

function foo() {
  console.log(`this within foo is ${this}`);
}
foo();

// console results
//   this within foo is [object Window]

Since foo belongs to the global scope (and not to some class or object), this still refers to the global object, Window.

Example 3: Within a non-arrow function in an object

let foo = {
  x: 10,
  whatIsThis: function () {
    console.log(`this equals foo? ${this === foo}`);
    console.log(`this.x is ${this.x}`);   
  }
};

foo.whatIsThis();

// console results
//   this equals foo? true
//   this.x is 10

We've got an object foo with a property x that is set to 10, and a property called whatIsThis that contains a function. The function prints out some information about this.

If we run foo.whatIsThis(), we can see that this is the object to which the function belongs, foo.

What if we start using that function in other objects, though?

Example 4: Within a non-arrow function defined elsewhere

let foo = {
  x: 10,
  whatIsThis: function () {
    console.log(`this equals foo? ${this === foo}`);
    console.log(`this.x is ${this.x}`);   
  }
};
// the above code is the same as the previous example

let bar = { x: 20 };
bar.whatIsThis = foo.whatIsThis;
bar.whatIsThis(); 

// console results
//   this equals foo? false
//   this.x is 20

Something to know about this example is that bar.whatIsThis is just a reference to foo.whatIsThis. They're the exact same function in memory, as we can show with:

foo.whatIsThis === bar.whatIsThis    // true

Despite this, the values we see printed to the console are different based upon how we call the function. Rather than seeing the same results from Example 3, the printed lines now reflect the fact that we called the function using bar.whatIsThis() instead of foo.whatIsThis().

To summarize, even though we're calling the exact same function, the value of this depends upon how we call it. It literally depends upon what is on the left side of the . in the function call.

Okay, now let's get weird. There are 3 situations inside a function where this just gives up and is set to the global object even though you're inside an object.

These situations are:

  1. When the function is defined as a local variable

  2. When the function is inside another function (a.k.a. "an inner function")

  3. When the function was passed as an argument

Example 5: Within a local variable

let foo = {
  bar: function () {
    let imALocalVariable = function() {
      console.log(this);
    }
    imALocalVariable();
  }
};

foo.bar();

// console results:
//   Window

Example 6: Within an inner function

let foo = {
  bar: function () {
    function imAnInnerFunction() {
      console.log(this);
    }
    imAnInnerFunction();
  },
};

foo.bar();

// console results:
//   Window

Example 7: When the function is passed as an argument

let foo = {
  whatIsThis: function () {
    console.log(this);
  },
  bar: function(ImAParameter) {
    ImAParameter();
  }
};

foo.whatIsThis();
foo.bar(foo.whatIsThis);

// console results:
//   Object { whatIsThis: whatIsThis(), bar: bar() }
//   Window

Note that in this example, when we run the function directly with foo.whatIsThis(), the object that prints to the console is foo itself. However, when we pass the same function as an argument to another function in foo that simply calls the passed-in function, this now refers to Window.

Next up, let's talk about methods in classes. First: static methods. Static methods are those that are defined on a class, and they're called on the class itself rather than on an instance of the class. They're akin to class methods in Ruby and other languages.

Inside of class methods, this refers to the class itself.

Example 8: Within a static method

class Foo {
  static bar() {
    console.log(this);
  }
}

Foo.bar();

// console results:
//   class Foo {
//     static bar() {
//       console.log(this);
//     }
//   }

For instance methods (a.k.a. prototypes method) in a JavaScript class, on the other hand, this refers to the current instance.

Example 9: Within an instance method

class Car {
  constructor(mileage) {
    this.mileage = mileage;
  }

  printMileage() {
    console.log(this.mileage);
  }
}

const newCar = new Car(5);
const oldCar = new Car(100000);
newCar.printMileage();
oldCar.printMileage();

// console results:
//   5
//   100000

In this example, this behaves in the way you probably expect within classes.

But, as you may expect, there are exceptions to the behavior of this within classes.

Example 10: Within event listeners

class Foo {
  constructor() {
    const button = this.addButton();
    button.addEventListener('click', function() {
      console.log(this);
    });
  }

  addButton() {
    const button = document.createElement('button');
    button.innerHTML = 'CLICK ME';
    document.body.prepend(button);
    return button;
  }
}

new Foo();

// console results, after clicking the button:
//   <button>CLICK ME</button>

The first exception to the behavior of this happens in functions used as event listeners. Inside those, by default this refers to event.currentTargetthis still refers to event.currentTarget if you set the event listener a little differently, like this:

class Foo {
  constructor() {
    const button = this.addButton();
    button.addEventListener('click', this.printThisToConsole);
  }

  printThisToConsole() {
    console.log(this);
  }

  addButton() {
    const button = document.createElement('button');
    button.innerHTML = 'CLICK ME';
    document.body.prepend(button);
    return button;
  }
}

new Foo();

// console results, after clicking the button:
//   <button>CLICK ME</button>

Example 11: Within an event listener using .bind

class Foo {
  constructor() {
    const button = this.addButton();
    button.addEventListener('click', this.printThisToConsole.bind(this));
  }

  printThisToConsole() {
    console.log(this);
  }

  addButton() {
    const button = document.createElement('button');
    button.innerHTML = 'CLICK ME';
    document.body.prepend(button);
    return button;
  }
}

new Foo();

// console results, after clicking the button:
//   Foo {}

You may have seen people use .bind, especially around event listeners like this. A thing you can do with .bind is to manually set the value of this. So we use this strategy in the above example to get the this in the event listener to be the same as the this in the other methods in that object.

A thing to be careful with about .bind, though, is that you can never re-bind. The first time you bind a function, it's permanent and un-undoable.

Example 12: Within an arrow-function event listener

class Foo {
  constructor() {
    const button = this.addButton();
    button.addEventListener('click', () => console.log(this));
  }

  addButton() {
    const button = document.createElement('button');
    button.innerHTML = 'CLICK ME';
    document.body.prepend(button);
    return button;
  }
}

new Foo();

// console results, after clicking the button:
//   Foo {}

Now for another twist. If you use an arrow style function instead of a function() {} style function for defining an event listener within a class, the value of this is the same as in other methods: the current instance of the class. Compare the code in Example 12 to that in Example 10 - the only difference is the way the function is defined.

Part of the popularity of arrow functions is due to the fact that it preserves the value of thisthis will be whatever it was in the context that the function was defined. This fact can make code more consistent and predictable.

Another interesting thing about arrow functions you can't change the value of this, even with .bind or other methods like .call or .apply.

References

Several of the code examples here were adapted from this excellent chapter by Dmitry Soshnikov. Many thanks to Dmitry for helping me learn all about this!

Previous
Previous

SciMed solutions wins a TBJ Life Sciences Award!

Next
Next

Arel, part III: Set subtraction - rails