`this`

The concept of `this` in JavaScript can be confusing. I decided I needed to understand this key feaure of JavaScript better. This is what I learned (pun somewhat intended).

Contents

    In JavaScript (JS), this is a dynamic keyword which can have different values associated with it depending on where and how it is invoked. Let’s look at how this works in the global execution context.


    When used outside of any function, this is a reference to the global object. In a browser setting this has a value of Window.

    console.log(this)
    // Window {parent: Window, opener: null, top: Window, length: 2, frames: Window...}

    In a node repl this has a value of global, however inside a node module this has a value of module.exports. The node engine runs each module of code inside of a wrapper function, and that wrapper function is invoked with the this value set to module.exports.


    For the purpose of this post, we'll be working in a browser environment.




    this in Function Calls


    In most cases, the value of a function’s this argument is determined by how the function is called. That means this can be different each time the function is executed. In a plain, undecorated function call, if you are not using strict mode, the value of this is set to the global object.

    function myFunc() {
      console.log(this)
    }
    
    myFunc()
    // Window {parent: Window, opener: null, top: Window, length: 2, frames: Window...}

    However, in strict mode, the function’s this value is set to undefined.

    function myFunc() {
      'use strict'
      console.log(this)
    }
    
    myFunc()
    // undefined

    It doesn’t matter whether the execution point is in strict mode, it matters whether the function definition is in strict mode. This matters when, for example, the code you are writing is written in strict mode, but a third party library you are making use of is not.


    If you're not using strict mode, it's easy to get unexpected results with this.

    function Person(firstName, lastName) {
      this.firstName = firstName
      this.lastName = lastName
    }
    
    const Gob = Person('Gob', 'Bluth')
    
    console.log(Gob)
    // undefined
    
    console.log(global.firstName, global.lastName)
    // "Gob"
    // "Bluth"

    Here we try to reference the variable Gob on line 8, but get undefined. This is because the properties firstName and lastName have been assigned to the global Window object, as illustrated on line 9.


    This is almost never the desired behaviour. If we tried the same code in strict mode, we'd get an error, preventing us from assigning properties to the global object.

    function Person(firstName, lastName) {
      'use strict'
      this.firstName = firstName
      this.lastName = lastName
    }
    
    const Gob = Person('Gob', 'Bluth')
    
    console.log(Gob)
    // Uncaught TypeError: Cannot set property 'firstName' of undefined

    If we call the function as a constructor using the new keyword, we no longer pollute the global namespace, and correctly assign the properties as intended.

    function Person(firstName, lastName) {
      'use strict'
      this.firstName = firstName
      this.lastName = lastName
    }
    
    const Gob = new Person('Gob', 'Bluth')
    
    console.log(Gob)
    // { firstName: "Gob", lastName: "Bluth" }




    this in Constructor Calls


    In JS, a function call preceded with the new keyword is a 'constructor' call. When a function is invoked as a constructor, a new object is created automatically, and that new object is then used for the this binding for the function call.


    Note: arrow functions cannot be used as constructor functions, in other words they cannot be invoked with the new keyword. Arrow functions have no implicit binding for this (more on that later).

    function Person(firstName, lastName) {
      console.log(this)
      this.firstName = firstName
      console.log(this)
      this.lastName = lastName
      console.log(this)
    }
    
    const Tommy = new Person('Tommy', 'Haverford')
    // Person {}
    // Person { firstName: 'Tommy' }
    // Person { firstName: 'Haverford' }

    Here we can see the output of the constructor function in action line by line. We can see the new object being linked to the Person function’s prototype. Since we didn’t include a return statement, the brand new object that was constructed for us was returned automatically.




    this in Method Calls


    When a function is called as a method of an object, the function’s this keyword is set to the object the function is defined in.

    const person = {
      firstName: 'Phil Mitchell',
      greeting() {
        console.log(`Hey, I'm ${this.firstName}!`)
      }
    }
    
    person.greeting()
    // "Hey, I'm Phil Mitchell!"

    Here, person is the receiver of the method call, and so that becomes the value of this.


    this is still bound to the object even if the method is created outside of the function, and added to the object as a property at a later time:

    function greeting() {
      console.log(`Hey I'm ${this.firstName}!`)
    }
    
    const person = {
      firstName: 'Super Hans'
    }
    
    person.sayHey = greeting
    
    person.sayHey()
    // "Hey I'm Super Hans!"

    Sometimes, methods are called several layers deep on an object using the dot notation. In this case, this is set to the very next property to the left of the method being called.


    Depending on the circumstances, the intended receiver of the this call can be lost. For example, if we initialise a new variable and set it's value to the object method, we lose the receiver:

    function greeting() {
      console.log(`Hey I'm ${this.firstName}!`)
    }
    
    const person = {
      firstName: 'Super Hans'
    }
    
    person.sayHey = greeting
    
    person.sayHey()
    // "Hey I'm Super Hans!"
    
    const greet = person.sayHey
    
    greet()
    // "Hey I'm undefined!"




    Specify this using .call() and .apply()

    We can attach methods to objects without explicitly declaring them using .call() or .apply().

    function greeting() {
      console.log(`Hey I'm ${this.firstName}!`)
    }
    
    const person = {
      firstName: 'Super Hans'
    }
    
    greeting.call(person)
    // "Hey I'm Super Hans!"
    
    greeting.apply(person)
    // "Hey I'm Super Hans!"
    
    console.log(person)
    // { firstName: "Super Hans" }

    .call() and .apply() invoke the function, and on lines 10 and 13 we can see that greeting has access to the this value of person.




    Specify this using .bind()

    In certain circumstances, we can lose our intended value of this. We can modify the value of this in some instances by using the .bind() method.

    const person = {
      firstName: 'Gob',
      greeting() {
        console.log(`Hi, my name is ${this.firstName}!`)
      }
    }
    
    setTimeout(person.greeting, 0)
    // "Hi, my name is undefined!"
    
    setTimeout(person.greeting.bind(person), 0)
    // "Hi, my name is Gob!"

    Here, .bind() creates a new greeting function, and permanently sets it's this value to person, i.e. person has permanently become the receiver of greeting. This is sometimes referred to as 'hard-binding'. Once this has been bound using .bind(), it cannot be changed, not even by using .call() or .apply().




    this in Arrow Functions

    Arrow functions do not have their own value for this. Instead, they inherit the value for this from the enclosing execution context, i.e. one level up.

    const outerThis = this
    
    const whatIsThis = () => {
      console.log(this === outerThis)
    }
    
    whatIsThis()
    // true

    The value of this in an arrow function cannot be manipulated in any way, even by using .bind(), .call() or .apply(). The this value of an arrow function will always be the same as it was at the place of the function definition.


    Arrow functions cannot be used as constructors, i.e. they cannot be invoked with the new keyword.

    const arrowFunc = () => null
    
    const aReferenceToTheArrowFunc = new arrowFunc()
    // TypeError: aReferenceToTheArrowFunc is not a constructor

    The transparent this binding of arrow functions is particularly useful when using them as call back functions, as seen in the example below:

    const counter = {
      count: 0,
      increment() {
        setInterval(function() {
          console.log(this.count++)
        }, 1000)
      }
    }
    
    counter.increment()
    // NaN

    The this value of setInterval is the global Window object, which doesn't have a count property associated with it, and so we get NaN. This example can be re-written as an arrow function, with better results:

    const counter = {
      count: 0,
      increment() {
        setInterval(() => {
          console.log(this.count++)
        }, 1000)
      }
    }
    
    counter.increment()
    // 1, 2, 3, 4, 5...




    this in Class Bodies

    Here we have a class, and everything looks like it's working as we might expect:

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
      }
    
      sayHi() {
        console.log(`Hi, my name is ${this.firstName}!`)
      }
    }
    
    const tommy = new Person('Tommy', 'Haverford')
    
    tommy.sayHi()
    // "Hi, my name is Tommy!"

    In the constructor, this refers to the newly created instance of the class. tommy is a newly created object, and is the receiver for the method call sayHi(). However, if we save the method reference to a variable, and try to invoke that variable, we lose the receiver, and we lose the intended this value along with it.

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
      }
    
      sayHi() {
        console.log(`Hi, my name is ${this.firstName}!`)
      }
    }
    
    const tommy = new Person('Tommy', 'Haverford')
    
    tommy.sayHi()
    // "Hi, my name is Tommy!"
    
    const introduceTommy = tommy.sayHi
    
    introduceTommy()
    // TypeError: Cannot read property 'firstName' of undefined

    This is because class bodies are implicitly set in strict mode, and we're attempting to invoke introduceTommy as an ordinary function. We can work around this by using the .bind() method:

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
      }
    
      sayHi() {
        console.log(`Hi, my name is ${this.firstName}!`)
      }
    }
    
    const tommy = new Person('Tommy', 'Haverford')
    
    tommy.sayHi()
    // "Hi, my name is Tommy!"
    
    const introduceTommy = tommy.sayHi.bind(tommy)
    
    introduceTommy()
    // TypeError: Cannot read property 'firstName' of undefined

    A variation of this would be to bind the sayHi method in the constructor:

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
        this.sayHi = this.sayHi.bind(this)
      }
    
      sayHi() {
        console.log(`Hi, my name is ${this.firstName}!`)
      }
    }
    
    const tommy = new Person('Tommy', 'Haverford')
    
    tommy.sayHi()
    // "Hi, my name is Tommy!"
    
    const introduceTommy = tommy.sayHi
    
    introduceTommy()
    // "Hi, my name is Tommy!"

    Another solution would be to use an arrow function:

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
      }
    
      sayHi = () => {
        console.log(`Hi, my name is ${this.firstName}!`)
      }
    }
    
    const tommy = new Person('Tommy', 'Haverford')
    
    tommy.sayHi()
    // "Hi, my name is Tommy!"
    
    const introduceTommy = tommy.sayHi
    
    introduceTommy()
    // "Hi, my name is Tommy!"

    Now we don't need to call .bind() because this is automatically set to the instance of the class.




    Thanks for reading!

    That's it. That's all I've got for today. I hope you found it useful. Feel free to get in touch if I've made any mistakes in this post, if you fancy a chat about this, or for anything else, by using one of the links below.