DEV Community

Nico Zerpa (he/him)
Nico Zerpa (he/him)

Posted on • Originally published at nicozerpa.com

JavaScript: Why Does `this` Work Like This?

"I hate this about JavaScript", "It creates so much confusion and so many bugs for zero gain.", "It's broken, Don't use it!" That's what many JavaScript devs think about the this keyword. For many of them, this has definitely given them the most trouble with more complex apps.

There are many articles out there about what the keyword means in different contexts, but now, I'd prefer to explain how this works, so that you can have a better understanding of it.

First of all, let's remember that the JavaScript object system is based on prototypes. What is a prototype? It's actually just an object that can be "inherited" by other objects. As prototypes are simple objects, they can have prototypes themselves.

When you try to access a property or method of a given object, first it searches the property on the object itself. If it can't find it, then it searches on the object's prototype. If it still can't find it, it searches on the prototype's prototype. And then, it keeps searching until the property is found. If it can't find the property anywhere, it's undefined.

Let's see an example:

function DogThatQuacks(name) {
    this.name = name
}
DogThatQuacks.prototype.bark = function() {
    return `${this.name} says "Quack!"`
}

const bartholomew = new DogThatQuacks('Bartholomew')

// Outputs 'Bartholomew says "Quack!"'
bartholomew.bark() 
Enter fullscreen mode Exit fullscreen mode

In the last line, the JavaScript engine first searches if the object bartholomew has a bark method. Since it hasn't (its only own property is name), then it looks into the prototype. It finds the method there, and finally executes DogThatQuacks.prototype.bark.

The thing is, the method bark exists in the object DogThatQuacks.prototype, not in bartholomew. How can the method access bartholomew.name? That's because the value of this depends on how you call the function.

You're eventually calling the method DogThatQuacks.prototype.bark, but you are calling it as a method of the object bartholomew. For that reason, this is a reference to bartholomew in this case. Now, let's play a little more with it:

// Outputs 'undefined says "Quack!"'
DogThatQuacks.prototype.bark()

// Outputs 'undefined says "Quack!"', but
// it throws an error in strict mode
const bark = bartholomew.bark
bark()
Enter fullscreen mode Exit fullscreen mode

In the first example, we're calling DogThatQuacks.prototype.bark directly! As you can guess, this is a reference to the prototype itself, which doesn't have the name property.

And in the second case, it will throw an error if you're using strict mode, and "undefined says Quack!" if not in strict mode. Why? because you're not calling bark as a method of an object, you're calling it as a simple function.

When you're calling functions in strict mode, this is not defined. And if the strict mode is not active, it references the global object. Again, the value of this depends on how you call the function.

More examples:

function makeDogBark(barkMethod) {
    console.log(barkMethod())
}
// Outputs 'undefined says "Quack!"', but
// it throws an error in strict mode
makeDogBark(bartholomew.bark)


DogThatQuacks.prototype.actuallyBark = function() {
    const internalFunction = function() {
        return `${this.name} now says "Woof!"`
    }

    return internalFunction()
}

// Outputs 'undefined now says "Woof!"', but
// it throws an error in strict mode
bartholomew.actuallyBark()
Enter fullscreen mode Exit fullscreen mode

In the first example, you're passing bartholomew.bark as an argument to the function makeDogBark. However, the function calls the argument barkMethod, that is, a simple function.

In the second case, you're again calling the simple function internalFunction, so this is undefined or the global object, depending on whether strict mode is enabled or not.

Also, you should consider that all of it also applies to classes. That's why classes in JavaScript are just syntactic sugar for prototypes:

class CatThatSaysMoo {
    constructor(name) {
        this.name = name
    }
    meow() {
        return `${this.name} says "Moo!"`
    }
}
const florence = new CatThatSaysMoo('Florence')

// Outputs 'Florence says "Moo!"'
florence.meow()

// Outputs 'undefined says "Moo!"'
CatThatSaysMoo.prototype.meow()

const meowFunction = florence.meow

// Throws an error, `this` is undefined
meowFunction()
Enter fullscreen mode Exit fullscreen mode

If you have to pass a method as an argument to a function, or if you need to store the method in a variable, you can use arrow functions (which "inherits" the this from the parent scope) or the bind method:

DogThatQuacks.prototype.actuallyBark = function() {
    const internalFunction = () => {
        // It inherits the `this` from
        // `DogThatQuacks.prototype.actuallyBark`
        return `${this.name} now says "Woof!"`
    }

    return internalFunction()
}

// Outputs 'Bartholomew now says "Woof!"'
bartholomew.actuallyBark()


// If fixes `this` as a reference
// to the object `florence`
const meowFunction = florence.meow.bind(florence)
// Outputs 'Florence says "Moo!"'
meowFunction()
Enter fullscreen mode Exit fullscreen mode

p.s. Did you like what you've read? Every week I send an email with free tips and insights to become a better JavaScript dev. If you're interested, click here to subscribe.

Top comments (0)