Understanding 'this' in Javascript

·

7 min read

Introduction

Tricky questions around this are very common in Javascript interviews

Unfortunately, in one such interview, I was not able to solve them. So I decided to study more on the this concept and document my understanding of it.

Most of the confusion around this comes from not completely understanding what it exactly refers to. Before diving into how to know what this refers to, let us understand why do we need this

Why this?

this helps functions to be reused with different objects. Here is an example of it

function introduce() {
    return this.name.toUpperCase();
}

function greet() {
    var greeting = "Hello, I'm " + introduce.call( this );
    console.log( greeting );
}

var person1 = {
    name: "Person1"
};

var person2 = {
    name: "Person2"
};

introduce.call( person1 ); // PERSON1
introduce.call( person2 ); // PERSON2

greet.call( person1 ); // Hello, I'm PERSON1
greet.call( person2 ); // Hello, I'm PERSON1

The above snippet needs a basic understanding of call and apply methods.

What it means is that the functions introduce() and greet() are being used with multiple objects (person1 and person2 ).

function introduce(context) {
    return this.name.toUpperCase();
}

function greet(context) {
    var greeting = "Hello, I'm " + introduce(context);
    console.log( greeting );
}
introduce(person1)
greet(person2)

Although we could have passed the objects to the functions like, like the example above,this keyword provides a more elegant way of passing an object reference.

Misconceptions about this

One common misconception about this is considering it to be referencing the function itself. To understand that this does not refers to the function itself. Let us understand it with an example.

function bark(i){
    console.log("barking", i)
    this.count ++;
}

bark.count = 0;

for(let i = 1 ; i < 5; i++){
    bark(i);
}
// barking 1
// barking 2
// barking 3
// barking 4

console.log(bark.count)
// 0

Although, the function bark() is called 4 times, the bark.count value still remains 0 . This clearly indicates that the this inside bark() does not refer to the function itself. When the code executes the bark.count, it adds a count property to the bark() function, but this.count inside does not refer to the same count function. Though the names are same, but the root objects are different. this.count is actually creating a new variable count in the window object.

To keep in mind, what this refers to is determined during the runtime. It is not based on how the function was declared but where the function is called. This is known as the call-site

What is a call-site?

The place where a function is called (not where it is declared).Yeah! its that's simple.

function a(){
    console.log("Hey");
}
a() //< --- call site for a

But to determine what this refers to (which is done during runtime), we need to keep in mind 4 rules.

Rules

1. Default Binding

Identifying a default binding is simple, if the function call just looks like the generic function call, its is default binding

function myFunction(){    
    console.log(this.a)
}
var a = 2;

myFunction() // 2

Inside myFunction() , this.a resolves to the global variable a , as variables declared in the global scope (var a) act as properties of the global scope ,so this.a === a . Therefore, myFunction() logs 2 in the console.

2. Implicit Binding

When a function call is preceded by a context object(also known as owing or containing object), this is when implicit binding comes into play.

E.g. :

function myFunction(){
    console.log(this.a)
}
var obj = {
        a : 2,
        myFunction : myFunction
    }

obj.myFunction(); // 2  <-- implicit binding
myFunction(); // undefined <-- default binding

Looking at the obj it might seem that myFunction() is owned by the obj but remember, to determine what this refers to, we need to see the call site. Here the call site is obj.myFunction() .

Therefore, obj owns myFunction not because, myFunction is inside obj but because, in the call site, myFunction() is preceded by obj .

Problems with implicit binding

An issue with implicit binding is that it sometimes loses it implicit binding and falls back to default binding

Consider the following example

function myFunction(){
    console.log(this.a)
}
var obj = {
        a : 2,
        myFunction : myFunction
    }
var myFunc = obj.myFunction;  (*)
var a = 3;
myFunc(); // <-- 3 . How??

Although, obj.myFunction (*) seems like implicit binding, its just a reference to myFunction() itself. Moreover, the call site here is myFunc() and this is just default binding, therefore, this.a inside myFunction() resolves to the global var a = 3

Another subtle way, implicit binding falls back to default binding is when we try to pass functions as a callback.

Consider this example

function myFunction(){
    console.log(this.a)
}
var obj = {
        a : 2,
        myFunction : myFunction
    }
function callFunctionAgain(callbackFn){
    callbackFn();    
}

var a = 3;
callFunctionAgain(obj.myFunction) // 3

This also results in 3 as the previous example. Test your understanding by taking help of the previous example on why it results to 3.

3. Explicit Binding

In implicit binding, we saw that to use a function we had to make a property reference to the function

function myFunction(){
    console.log(this.a)
}
var obj = {
        a : 2,
        myFunction : myFunction // <-- property myFunction(left of ':') refering to
                                                                //         function myFunction
    }

But what if we wanted to use a function without changing the obj and force a function call on that object i.e. we do not want to create a property to reference the function as shown in the example above.

In such cases, we need the help of call() and apply().

Example :

function myFunction(){
    console.log(this.a)
}
var obj = {
        a : 2
    }

myFunction.call(obj) //<-- 2

call() and apply() work the same with the only difference in the way the additional parameters are passed on to them.

While this blog is not intended to explain the use of call, apply (and bind). I would recommend googling about it if you aren't familiar with their concepts.

But using call/apply still does not solves the issue of binding falling back to default binding as mentioned in the previous section.

However, using bind solves this issue. Bind returns a new function with this already set to the context you specified

function myFunction(){
    console.log(this.a)
}
var obj = {
        a : 2        
}
var a = 10
var myFunc = myFunction.bind(obj)

myFunc() // 2

4. new Binding

In JavaScript, when a function is called with a new keyword, it creates a new object with the this referring to the newly created object. It has not relation with the traditional class-oriented behavior.

Pretty much all functions can be called with a new keyword. For our current discussion, when we use new with a function, the following things happen

  1. a new object is created
  2. this refers to the newly created object
  3. this new object is prototype linked Read More)
  4. if the function does not return its own object, the new invoked function call will return the newly created object.

Example :

function myFunction(a) {
    this.a = a;
}

var myFunc = new myFunction( 2 );
console.log( myFunc.a )

Order of Precedence of the rules.

The order of precedence are in the following manner

new > explicit > implicit > default

this for arrow functions

In case of arrow functions, this refers to the arrow function's lexical scope

Example:

function myFunction() {    
    return (a) => {        
        console.log( this.a );
    };
}

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
};

var myFunc= myFunction.call( obj1 );
myFunc.call( obj2 ); // 2, not 3

The this of myFunction() is set to obj1 . The lexical binding of arrow functions cannot be over-ridden even with new. When myFunction.call( obj1 ) executes, this gets set to obj1.

Therefore, calling myFunc with obj2 still resolves to the a in obj1

Conclusion

To sum up, to determine what this refers to, we need to carefully look for the call site and determine which of the above rules is being applied in the following precedence.

  1. If the function is called with new, this will be the newly created object.
  2. If the function is called with call, apply or bind use the specified object in the first parameter.
  3. If the function is called with an object e.g: obj1.myFunc(), this is the containing object.
  4. For default binding, this resolves to the global object.

Hope this blog gave you a better understanding of how this works. Do drop your feedback and share it with your friends if you find it useful.

I write blogs documenting the things I learn.

You can follow me on Twitter and LinkedIn