DEV Community

cclintris
cclintris

Posted on

JavaScript: this

JavaScript: All about JS keyword this

Intro

As a frontend engineer, this is a keyword that you will see very often. It is a pretty fundamental concept, yet an extremely important onf if you want to become better at javascript, or as a frontend engineer. In this article, I am going to cover all the knowledge that is entailed to this.

global environment

For convenience, we'll refer global environment as ge below.

Basically, if you call this directly in ge, you will get a window object. And it is strictly equal.

this === window; // true
Enter fullscreen mode Exit fullscreen mode

And of course, the code below remains true:

this.document === document;
Enter fullscreen mode Exit fullscreen mode

Furthermore, if you try to add a new attribute to window directly by this, it is also feasible. For example:

this.myName = "Tris";
console.log(myName); // Tris
console.log(window.myName); // Tris

this.myName === window.myName; // true
this.myName === myName; // true
Enter fullscreen mode Exit fullscreen mode

Even if you apply the strict mode by adding use strict at the top of file, it still behaves as if this basically points to the window object.

functional environment

For convenience, we'll refer functional environment as fe below.

In fe, if you call this, its reference depends on how you call this function. If you call the function directly, this will refer to the window object directly, this is called a Sample Call.

For example:

function fn() {
  return this;
}

fn() === window; // true
Enter fullscreen mode Exit fullscreen mode

However, most of the time when we're actually developing, we have a preference to avoid calling a function directly(Sample Call). Take an example of Vue developing:

const app = new Vue({
  el: "#app",
  data: {
    myName: "Ray",
  },
  methods: {
    getName() {
      console.log(this.myName); // Ray
    },
  },
  created() {
    this.getName();
  },
});
Enter fullscreen mode Exit fullscreen mode

Looks just fine, right? But let's see what will happen by just changing the code a little bit:

const app = new Vue({
  el: "#app",
  data: {
    myName: "Ray",
  },
  methods: {
    getName() {
      const array = [1, 2, 3];
      array.forEach(function () {
        console.log(this.myName); // undefined * 3 ,WTF?
      });
    },
  },
  created() {
    this.getName();
  },
});
Enter fullscreen mode Exit fullscreen mode

Now, that's very weird right? Why does a simple forEach result in such bizarre outputs? Well, let's first take a look at forEach source code:

Array.prototype.forEach = function(callback) {
    for(let index = 0; index < this.length(); index++> {
        callback(this[index], index, this);
    })
}
Enter fullscreen mode Exit fullscreen mode

We can see that the function passed to forEach is sample called, resulting in a conflict of this, therefore the reason of this reference misdirect.

How will we deal with this problem, or if we put it in other words, what is a better way of programming in order to prevent and elude these strange confusing this reference problems?

The first way is to declare a variable to reserve this.

const app = new Vue({
  el: "#app",
  data: {
    myName: "Ray",
  },
  methods: {
    getName() {
      const vm = this;
      const array = [1, 2, 3];
      array.forEach(function () {
        console.log(vm.myName); // Ray * 3,is Good!
      });
    },
  },
  created() {
    this.getName();
  },
});
Enter fullscreen mode Exit fullscreen mode

The second way, it to use the arrow function.

const app = new Vue({
  el: "#app",
  data: {
    myName: "Ray",
  },
  methods: {
    getName() {
      const array = [1, 2, 3];
      array.forEach(() => {
        console.log(this.myName); // Ray * 3,is Good!
      });
    },
  },
  created() {
    this.getName();
  },
});
Enter fullscreen mode Exit fullscreen mode

By either way, the problem seems to be solved. For the first way, by creating a variable to store this in the Vue component separates different this references under different enrivironment. But the second way seems somewhat confusing. Why do a simple arrow function takes care of the problem? Don't worry, everything will be explained later.

So basically, when you call a function directly, or even pass an anonymous function as a parameter, it is often a sample call, which might result in this problems.

const app = new Vue({
  el: "#app",
  data: {
    myName: "Ray",
  },
  methods: {
    getName() {
      (function () {
        console.log(this.myName); // undefined,WTF...
      })();
    },
  },
  created() {
    this.getName();
  },
});
Enter fullscreen mode Exit fullscreen mode

object function

Another very common case is when we put this in a javascript object's function. This is also a very dangerous spot that may lead to a this reference misdirect problem.

var myName = "oh No!";

var obj = {
  myName: "Tristan",
  fn: function () {
    console.log(this.myName);
  },
};

obj.fn(); // Tristan

var fn = obj.fn;
fn(); // oh No!,WTF?
Enter fullscreen mode Exit fullscreen mode

Notice that the output kind of makes no sense as you call the same function fn in object obj. Notice that we used var instead of let, const.

If we use let or const:

const myName = "oh No!";

const obj = {
  myName: "Ray",
  fn: function () {
    console.log(this.myName);
  },
};

obj.fn(); // Ray

var fn = obj.fn;
fn(); // undefined,WTF?
Enter fullscreen mode Exit fullscreen mode

In this article we're not really gonna spend too much time explaining the differences between var, let and const, which we will in the future.

To find out this references in a js object is pretty simple though. You just need to look which object if this in when called. Take fn() above as an example. this is called under the object obj, hence this refers to obj.

new constructor

When we use new before the function that we are calling, in javascript, it treats the function as if it's an object, where this becomes a basic property of the object.

function fn(myName) {
  this.myName = myName;
}

const newFn = new fn("Tris");

console.log(newFn.myName); // Tris
Enter fullscreen mode Exit fullscreen mode

Just like an plain js object:

const obj = {
  myName: "Tris",
};

console.log(obj.myName); // Tris
Enter fullscreen mode Exit fullscreen mode

DOM

When you use this on DOM elements, when you use it with addEventListener, this will then refer to the DOM element itself on matter what.

For example:

<button type="button" class="btn">1</button>
<button type="button" class="btn">2</button>
<button type="button" class="btn">3</button>
<button type="button" class="btn">4</button>
<button type="button" class="btn">5</button>
Enter fullscreen mode Exit fullscreen mode
const buttons = document.querySelectorAll(".btn");

buttons.forEach(function (button) {
  button.addEventListener("click", function () {
    console.log(this); // The DOM you click
  });
});
Enter fullscreen mode Exit fullscreen mode

When you declare an event listener by adding addEventListener on a specific DOM element, it actually is going to wait in the future at some point when you conduct a certain act, such as click in this case. Meantime, the callback function will be binded to the DOM element, so basically, you can think if the code above as below, if it helps to understand:

const click = {
  button: '<button type="button" class="btn">1</button>',
  addEventListener() {
    console.log(this);
  },
};

click.addEventListener();
Enter fullscreen mode Exit fullscreen mode

However, when using this with DOM elements, sometimes you find this refering to strange things. Which is when you use it with arrow functions.

const buttons = document.querySelector(".btn");

buttons.forEach(function (button) {
  button.addEventListener("click", () => {
    console.log(this); // window ???
  });
});
Enter fullscreen mode Exit fullscreen mode

Well, you might remember that above you've just read about arrow functions. Actually, arrow functions indeed include some important points that has to do with this, so let's continue this this topic by taking a look at arrow functions.

arrow function expressions

Before we actually talk about arrow functions, we have to understand the differences between arrow functions and normal functions.

In MDN docs, it is said that an arrow function does not have its this, arguments, super and new.target. So, the question is, if arrow functions do not have their own this, then where does this refer to when using arrow functions.

It turns out that when using arrow functions, this will refer to its outer domain, the parent
domain.

Let's take a look again at an example which seems confusing at first:

const app = new Vue({
  el: "#app",
  data: {
    myName: "Ray",
  },
  methods: {
    getName() {
      const array = [1, 2, 3];
      array.forEach(() => {
        console.log(this.myName); // Ray * 3,is Good!
      });
    },
  },
  created() {
    this.getName();
  },
});
Enter fullscreen mode Exit fullscreen mode

In the code above, if we write is as array.forEach(function() {...}), the output would be undefined or window depending on the environment you're working with. The reason is that this results in a sample call as previously mentioned. However, by using arrow functions, due to the absent of this of arrow functions' inherit nature, it then follows its way up to refer to the nearest this, which turns out to be the vue instance in this case.

references

Top comments (0)