DEV Community

Ezra Schwepker
Ezra Schwepker

Posted on

JavaScript; what's the meaning of `this`?

wtf mate
Alright JavaScript, what the hell. We were cool. Now, I just don't know anymore. I was hanging out with Ruby for the past couple months and I got used to how consistent and sensible self is. Now I'm back and things are...weird.

What I'm struggling with most is whether or not JavaScript's implementation of this is a necessary evil with a first-class function language, or just another bad idea that we're stuck with.

I'm going to condense for you some thoughts gleaned from MDN, Kyle Simpson and the ECMA 2020 Spec. (just for fun, search for "this" in the Spec 😂)

For those that aren't already frustrated by this, let's get you on board the "wtf, JavaScript" train. this should allow us to write functions with an reference to the owning object without having to hardcode the name of the object.

function Person(name) {
    this.name = name;
    this.printName = function() { console.log(this.name); };
}

const joe = new Person("Joe");
const sal = new Person("Sal");

joe.printName();    //> "Joe"
sal.printName();    //> "Sal"

So that's pretty damn useful. We've just used this to attach a property and a method to an object and to return the property using the method.

However, methods in JavaScript are just functions, and functions are values which can be bound to new variables, given as arguments to other functions, and returned from functions. Which means we can do this:

const printName = joe.printName;
printName;    //> ƒ () { console.log(this.name); }

printName();  //> (nothing prints)

Now that the function printName is being called from outside of the object context in which it found the value of this.name, it has no idea what this.name should resolve to. But it does actually know what this refers to:

const name = "Mike";    // declared in Global Scope

printName();   //> "Mike"

It turns out that the global object (as well as modules) has its own this property. If the function you are executing doesn't have a this defined by its execution context, it will look default to the global this.

console.log(this);    //> Window { ... }

console.log(window.globalThis);    //> Window { ... }

That still isn't that confusing. Let's go deeper.

function anExample() {
    console.log(this);
    function aNestedExample() {
        console.log(this);
    }
    aNestedExample();
}
anExample();
//> Window { ... }
//> Window { ... }

const anObj = {};
anObj.aMethod = anExample;

anObj.aMethod();
//> { aMethod: f }
//> Window { ... }

And that right there is the heart of the matter. Even though we have attached the anExample function to an object as a method, the function inside of it is still referring to the global object.

isn't that weird?

Why?

Turns out there are 2 important factors:

1) Is "use strict" mode enabled?
2) How is the function invoked?

Is "use strict" enabled?

There's an additional factor! Let's slightly modify the example above to use "use strict"

"use strict";

function anExample() {
    console.log(this);
    function aNestedExample() {
        console.log(this);
    }
    aNestedExample();
}
anExample();
//> undefined
//> undefined

const anObj = {};
anObj.aMethod = anExample;

anObj.aMethod();
//> { aMethod: f }
//> undefined

If "use strict" is enforced, JavaScript will not search outside of the function's execution context for a this value, so if the function does not define one for itself, this resolves to undefined.

How is the function invoked?

If the function is invoked as a function without any object provided as a context for this to refer to, then this will refer to the global object.

function example() {
    console.log(this);
}
example();    //> Window { ... }

const anObj = {
    property: 5,
    method: function() {
        console.log(this);
        function noThis() {
            console.log(this);
        }
        hasThis();
    }
}
anObj.method();
//> { property: 5, method: f }
//> Window { ... }

this is not lexically scoped. Even if it is contained within a function which has a this value and is lexically located within an object, it still refers to the global object.

What actually matters is whether or not the function is invoked as a method:

const anObj = {
    property: 5,
    method: function() {
        console.log(this);
        return function noThis() {
            console.log(this);
        }
    }
}
const anotherObj = {};

anObj.method()();
//> { property: 5, method: f }
//> Window { ... }

anotherObj.asAMethod = anObj.method();    
//> { property: 5, method: f }

anotherObj.asAMethod();
//> { asAMethod: f }

In this example, even though the function noThis is lexically within a method on another object which does have a this when invoked it doesn't have its own this unless it is invoked as a method on a new object. Otherwise refers to the global object.

So the actual notation that you use to invoke a method matters.

anObject.method;    // Dot Notation: this == true
anObject["method"]; // Computed Access: this == true
asAFunction();      // Function invocation: this == false

The value of this is generated each time the function is invoked. Any function which isn't invoked as an object method will not generate a this value.

But there's one final twist. Object generator functions and classes do have a this value when they are invoked, even though they aren't being called as a method:

class Person {
    constructor(name) {
        console.log(this);
        this.name = name;
    }
}
const joe = new Person("Joe");
//> Person {}

function Person(name) {
    console.log(this);
    this.name = name;
}
const joe = new Person("Joe");
//> Person {}

And that makes sense, they're generating an object and need to be able to reference the object while it is being created in order to add properties and methods to it.

If we define a method on the prototype, it still refers to the created object correctly, because we're using a method call!

Person.prototype.newMethod = function () {
    console.log(this);
}
joe.newMethod();    //> Person { name: "Joe" }

Ok, that's actually kind of sensible, but it sets a trap with nested functions where you aren't calling the nested function as a method.

There's two common solutions to that problem:

1) Save the value of this as a variable, e.g. that, _this, self or _self.

const anObj = {
    property: 5,
    method: function() {
        const that = this
        function noThis() {
            console.log(this);
            console.log(that);
        }
        noThis();
    }
}
anObj.method();
//> Window { ... }
//> { property: 5, method: f }

Saving the value of this allows it to be used with normal lexical scoping rules.

2) Use Arrow Functions

const anObj = {
    property: 5,
    method: function() {
        const that = this
        const noThis = () => {
            console.log(this);
            console.log(that);
        }
        noThis();
    }
}
anObj.method();
//> { property: 5, method: f }
//> { property: 5, method: f }

Arrow functions do not define their own this value, instead they look outwards up the lexical scope chain for a this value, acting just like you wish this would.

However, because they do not define their own this if you don't nest them inside of function defined with the function keyword and call that as a method from an object, your arrow function won't know what the value of this is either:

const anObj = {
    method: () => console.log(this)
}
anObj.method();    //> Window { ... }

So if you want to use this:

1) Use dot . or bracket [ ] notation when calling your function.
2) Use function to define your methods instead of arrow functions.
3) Either bind the value of this or use arrow functions within your methods for lexical scope.
4) Don't use this unless you expect to use the function as a method or have prepared for it to refer to the global object instead.

Ok, but why?

Whether or not JavaScript's solution to this is a good idea should be judged against the problem that it faces. Unlike languages like Ruby where objects own their methods and those methods are typically defined within or upon the object which self will refer to, JavaScript doesn't have formal methods.

Methods in JavaScript are actually key:value pairs on an object which stores the reference value to the function object. When calling a method with dot or bracket notation, the expression object.method() is actually two parts: object.method and the function invocation. object.method evaluates to myMethodFunction which is then is then invoked.

Unlike Ruby, where the object receives a message and determines if it can respond to the message based upon the methods defined upon it, all JavaScript objects know is what keys they have defined and what values correspond to those keys. Since the functions used as methods can be defined anywhere and may be referenced by many different objects, there's no clear owner of any function and JavaScript is forced to determine this based upon the context by which it was called rather than how it was defined.

So this makes sense. It's complicated, it's tricky, and having this refer to global rather than lexical scope is a bad idea, but now that we understand that, we anticipate it and just use that or => instead!

Top comments (4)

Collapse
 
lordliquid profile image
Robert Cutright • Edited

Another useful thing to note here..
When trying to obtain 'this' within an arrow function, 'this' could be referred to by the variable name of the object:

// let lib = require('custom-library');
const obj = { // note: the 'obj' can be used to access the 'this' you were looking for.
    method: () => console.log(obj), // 'obj' represents itself as 'this'
    another: function() {
        console.log(obj); // even better to use 'obj' in a function method scope too, because 'this' would walk up the proto chain and give you the global.
    },
    getMethod: () => {
        return () => {
            console.log(obj);
        }
    }
}

// Let's try it
obj.method();   // { method: [Function: method], another: [Function: another], getMethod: [Function: getMethod] }
obj.another();  // { method: [Function: method], another: [Function: another], getMethod: [Function: getMethod] }

// Let's try the other problem and see if this solves it as well.
let anotherObj = obj;
anotherObj.method(); // { method: [Function: method], another: [Function: another], getMethod: [Function: getMethod] }
anotherObj.another(); // { method: [Function: method], another: [Function: another], getMethod: [Function: getMethod] }

let another = obj.another;
another(); // { method: [Function: method], another: [Function: another], getMethod: [Function: getMethod] }

let method = anotherObj.getMethod();
method(); // { method: [Function: method], another: [Function: another], getMethod: [Function: getMethod] }

// Another test case
const func = () => {
    console.log(func);
}

func(); // [Function: func]

With that being said.. would it be better to just forget about the use of this and just use the objects name as a replacement?

disclaimer: Though, I haven't found any note-able pitfalls to this approach yet. I'd encourage you all to do your own research before using this method.

Collapse
 
hozefaj profile image
Hozefa

Great post.

For the benefit for visual learners(myself included), the series on egghead is worth the watch.

Collapse
 
moresaltmorelemon profile image
Ezra Schwepker

Wow, those are some excellent videos. Incredibly clear.

I'll have to look into subscribing in order to watch them all, but if the rest of their content is on par it will be worth it.

Thanks!

Collapse
 
hozefaj profile image
Hozefa

It's totally worth it. They keep releasing tons of courses over the year. Worth the money.