JavaScript - this keyword explained

JavaScript - this keyword explained

The this keyword is one of the most widely used features of JavaScript. It is without doubt one of the hardest things to grasp. This blog post aims to help you understand the value of this in different contexts.

First and foremost, we should stop trying to make the this keyword in JavaScript like the this keyword in other languages such as C++, Java, and PHP. There, the this keyword represents the instance of the current object and it only makes sense inside a method of a class. You cannot use it outside of the method.

When it comes to JavaScript, this can be used both in the global context and function context. In the global context, this references the global object, which is the window object on the web browser or the global object on Node.js.
For example:

this.dog = 'German Shepherd';
console.log(window.dog); // 'German Shepherd'

In functions, this references the execution context of that function call. It is determined completely by how the function is called.
This may seem unusual for a lexically scoped language like JavaScript but, it is not possible to simply look at a function's definition and tell what the this keyword points to. What it points to depends on how the function was called.
Let's see all the ways that a function can be called and see how the this keyword behaves.

4 ways to call a function

  1. Implicit Binding

    Implicit Binding is applied when you invoke a function in an object using the dot notation.

     let dog = {
        breed: "Poodle",
        getBreed() {
          return console.log(this.breed);
       },
      };
    
     dog.getBreed()
    

    dog.getBreed() says:

    invoke getBreed() with the this keyword pointing at thedog object

    Implicit Binding is very useful for sharing behavior between different objects.

     function getBreed() {
       return console.log(this.breed);
     }
    
     let dog = {
       breed: "Poodle",
       getBreed,
     };
    
     let cat = {
       breed: "Birman",
       getBreed,
     };
    
     dog.getBreed(); //'Poodle'
     cat.getBreed(); //'Birman'
    

    Notice how dog and cat are two different objects with different data, but they are sharing the same getBreed() function. There are no two different getBreed() functions in memory, there is just one.

  2. Explicit binding

    You can invoke a function with the call() method (or apply()) and you can provide as a first argument a value for this.

     let dog = {
       breed: "Pug",
     };
    
     getBreed.call(dog); //'Pug'
    

    What getBreed.call(dog) is basically saying is:

    invoke getBreed function, with a particular context that I am going to specify as first argument (dog).

    A variation of explicit binding is called hard binding.
    You can use bind(), to make a function that, no matter how it is called, it has a particular this value.

    NOTE: bind() doesn't invoke the function, it just creates a new one which is bound to a particular this context.

    Let's see the following code:

     const dog = {
       breed: "Poodle",
       getBreed() {
         return console.log(this.breed);
       },
     };
    
     const myDog = dog.getBreed;
     myDog(); //'undefined'
     const myBoundDog = dog.getBreed.bind(dog);
     myBoundDog(); //'Poodle'
    

    myDog() prints undefined because this refers to the global context. And myBoundDog() prints Poodle because dog.getBreed.bind(dog) returns a new function bound to dog.

  3. The new keyword

    The new keyword is the third way to invoke a function. It's purpose is to call a function with the this keyword pointing to a new empty object.

     function Dog(breed){
      this.breed = breed
     }
     const myDog = new Dog("Bulldog")
    

    First it creates a new empty object {}. It uses that empty object as value for this. It sets a breed property with the value "Bulldog" and returns that object.

  4. Default binding

    This one is just a simple function invocation.

     const breed = "Bulldog"
     function getBreed() {
       return console.log(this.breed);
     }
     getBreed() // 'Bulldog' (in non-strict mode)
    
     const breed = "Bulldog"
     function getBreed() {
       "use-strict"
       return console.log(this.breed);
     }
     getBreed() // TypeError
    

    The reason why in strict mode we get a type error is because in strict mode, if you invoke a function without any this value (implicit binding, explicit binding or with the new keyword), the default behaviour is to leave it as undefined.
    And accessing a property on the undefined value throws the Type Error. Which is good because calling a this aware function without any this is probably a mistake.

Arrow Functions

Arrow functions do not define a this keyword no matter how are invoked. That means the this keyword inside an arrow function, behaves the same way as any other variable, which means it is going to lexically resolve to the nearest enclosing scope, that has defined a this keyword.

We can verify that it doesn't define a this keyword simply by trying to invoke an arrow function with the new keyword:

const myArroyFn=(breed)=>{this.breed=breed}

new myArroyFn("Poodle") // 'TypeError'