What is this? Javascript?

You got this text on a road.

Photo By sydney Rae

This post summarizes the this binding principles explained in the book this & Object Prototypes, by Kyle Simpson. It's one of the book in the series You Don't Know JS. If you want to learn more about JavaScript from the roots, I highly recommend you reading it.

Call-Stack and Call-Site#

To understand what this holds, we first need to understand about call-stack and call-site.

1function parent() {
2 // call-stack is: `parent`
3 // so, our call-site is in the global scope
4
5 console.log( "parent" );
6 child(); // <-- call-site for `child`
7}
8
9function child() {
10 // call-stack is: `parent` -> `child`
11 // so, our call-site is in `parent`
12
13 console.log( "child" );
14 grandchild(); // <-- call-site for `grandchild`
15}
16
17function grandchild() {
18 // call-stack is: `parent` -> `child` -> `grandchild`
19 // so, our call-site is in `child`
20
21 console.log( "grandchild" );
22}
23
24parent(); // <-- call-site for `parent`

As we see in the above code, each function has a call-site, that refers to the site it is being called from. parent() is called from global scope, and child() is called from parent scope and so on.

Similarly, call-stack holds the stack of scopes from which the current function is being executed. For, e.g. grandchild has stack parentchildgrandchild

The Four Rules#

The Mystery, 'what this refers to?' could be cracked by checking if this falls on one of these four rules.

Default Binding#

This is the default rule, which would be applied if none of the other rules seem to apply.

1function foo() {
2 console.log(this.a);
3}
4
5var a = 2;
6
7foo(); // 2

In the above snippet, the call-site of foo() is in global scope, hence this would check for global object property with the key a. The variables declared in global scope, are same as global object properties. Hence, a refers to the global a.

If our this execution is in strict mode, this rule would still apply, but global object would return undefined instead of 2.

Implicit Binding#

This rule checks for context object which has this execution. In other words, the this here refers to the containing object. It's easy to see this in example rather than explaining it with words.

1function foo() {
2 console.log(this.a);
3}
4
5var obj = {
6 a: 2,
7 foo: foo
8};
9
10obj.foo(); // 2

The above snippet has this reference, when line number 2 is executed, it's containing object is checked, if it is present, then this refers to that object. It can be visualized as below:

1var obj = {
2 a: 2,
3 foo: function foo() {
4 console.log(this.a); // this refers to obj
5 }
6};
7
8obj.foo(); // 2

The important thing about this binding is, only the top/last level of the object is considered for this binding.

1function foo() {
2 console.log(this.a);
3}
4
5var last = {
6 a: 42,
7 foo: foo
8};
9
10var first = {
11 a: 2,
12 last: last
13};
14
15first.last.foo(); // 42

As seen in above snippet, when this is encountered in foo() the last level object which has a is taken as 42 and not the a from first object.

Explicit Binding#

As the name suggests, JavaScript engine provides us option to explicitly specify an object to be used for this reference. call() and apply() functions are examples for explicitly binding an object to be used for this reference.

1function foo() {
2 console.log(this.a);
3}
4
5var obj = {
6 a: 2
7};
8
9foo.call(obj); // 2
10
11var obj2 = {
12 a: 55
13};
14
15foo.call(obj2); // 55

apply() function does exactly similar to call(), it just differs in the way the additional parameters were passed to that function.

1// CALL
2function foo(arg1, arg2) {
3 console.log(this.a + arg1 + arg2);
4}
5
6var obj = {
7 a: 2
8};
9
10foo.call(obj, 2, 2); // 6
11
12
13// APPLY
14function foo(...arr) {
15 console.log(this.a + arr[0] + arr[1]);
16}
17
18var obj = {
19 a: 2
20};
21
22foo.apply(obj, [2, 2]); // 6

So, structurally, these two executions are equivalent.

1func.call(thisContext, ...args);
2func.apply(thisContext, args);

The new Binding#

After so many years, ES6 bundled with classes out of the box, with which we can create relationships between entities in an object-oriented way. However, strictly speaking, when compared with class-oriented languages like C# or Java, there are no real classes in JavaScript.

Okay why are we into class discussion now? What this has to do with this in JavaScript?

To understand the fourth rule, we must understand classes, or at least to some extent. We will start with a simple class.

1class NameBuilder {
2 constructor(name){
3 this.name = name;
4 }
5
6 tellName(){
7 console.log('Your name is ' + this.name);
8 }
9}
10
11const obj = new NameBuilder('Bharathi');
12
13obj.tellName(); // "Your name is Bharathi"

When you go through the above example, the following is what you would assume.

When new keyword is encountered, JS would call the constructor of our class NameBuilder, which sets this.name to the value we pass in. Then in the object created, we will have the class method tellName() attached to it, and when we execute it, it would print the name set by the constructor.

Though this is a valid assumption and a valid mental modal, on the interior of JavaScript, it's not how the class instances would be executed. First, there are no constructors in JavaScript. In other words, constructors are just plain JS functions.

We will see the same class example with simple function for our understanding.

1function NameBuilder(name) {
2 this.name = name;
3 this.tellName = function tellName(){
4 console.log('Your name is ' + this.name)
5 }
6}
7
8const obj = new NameBuilder('Bharathi');
9
10obj.tellName(); // "Your name is Bharathi"

There is no constructor involved here, but the execution works similar to classes. What exactly is happening here?

When a function is invoked with new in front of it, the following things happen automatically.

  1. A Brand new object is created (aka, constructed) out of thin air
  2. the newly constructed object is [[Prototype]]-linked
  3. the newly constructed object is set as the this binding for that function call
  4. unless the function returns its own alternate object, the new-invoked function call will automatically return the newly constructed object.

Don't worry, let's follow the steps one by one,

1const obj = new NameBuilder('Bharathi');
2
3// 1. A Brand **new object** is created (aka, constructed) out of thin air
4// creates new object {}
5
6// Lets skip #2 for now
7
8// 3. the newly constructed object is set as the `this` binding
9// for that function call
10// When the below function is called, `this` points to {}
11// Since {} has no `name` or `tellName` properties, they would be set
12
13function NameBuilder(name) {
14 this.name = name;
15 this.tellName = function tellName(){
16 console.log('Your name is ' + this.name)
17 }
18}
19
20
21// 4. unless the function returns its own alternate object,
22// the new-invoked function call will automatically return the
23// newly constructed object.
24// Our function does not return anything, hence, the newly
25// created object with two properties, is returned
26
27const obj = new NameBuilder('Bharathi');
28
29// Becomes:
30
31const obj = {
32 name: 'Bharathi',
33 tellName : tellName() {
34 console.log('Your name is ' + this.name)
35 }
36}
37
38// So, now when we execute `obj.tellName()` which has `this.name`
39//would point to obj.name,
40
41"Your name is Bharathi"

Just to make sure it is working as we think, let's tweak that function to return a different object and check.

1function NameBuilder(name){
2 this.name = name;
3 this.tellName = function (){
4 console.log('Your name is ' + this.name)
5 }
6
7 return {
8 name: "Rajaraam",
9 tellName: function tellName() {
10 console.log("Your name is " + this.name)
11 }
12 }
13}
14
15
16const obj = new NameBuilder('Bharathi');
17
18obj.tellName(); // "Your name is Rajaraam"

As seen above, as per the point number 4, since the function returns a different object, this.name points to Rajaraam. The newly created object is discarded.

Coming back to new binding, since new is responsible for creating a new object and setting that object as this context, this rule is called new Binding.

Recap#

Let's recap the four rules that we saw about this binding.

  1. Is the function is called with new then this would be newly constructed object.
var bar = new foo();
  1. Is the function is called with explicit bind functions like call or apply? Then, this would be the context passed to those functions.
var bar = foo.call(obj1);
  1. Is the function executed with containing object? Then this would be that object.
var bar = obj1.foo();
  1. Otherwise, default binding applies which returns global property, if present in 'non-strict' mode and undefined in 'strict' mode.
var bar = foo();

Arrow Functions#

When we read through JavaScript articles on Internet, I'm pretty sure we have gone through the concept of this's behavior with arrow functions. The question to be asked now is, 'How does it play along with the rules mentioned above?'

The answer is, it does not.

The this inside an arrow function adopts this binding from the enclosing function or global scope. That's it. No other gotchas!

Let's see this with an example.

1function HOCFunction() {
2 // return an arrow function
3 return () => {
4 // `this` here is lexically adopted from `foo()`
5 console.log(this.a);
6 };
7}
8
9var obj1 = {
10 a: 2
11};
12
13var obj2 = {
14 a: 3
15};
16
17var anotherFunction = HOCFunction.call(obj1);
18
19anotherFunction.call(obj2); // 2, not 3

We are binding HOCFunction to obj1 and saving it in anotherFunction.

Now, when we call anotherFunction with explicit binding of obj2, we expect it to take this.a from obj2, but it's not. Since this is arrow function, which does not have its own this, it would be lexically referenced, in our case from HOCFunction and hence it is printed as 2.

So, instead of returning arrow function, if we have returned normal function, 3 would be printed!

1function HOCFunction() {
2 return function ReturnedFunction(){
3 console.log(this.a);
4 };
5}
6
7var obj1 = {
8 a: 2
9};
10
11var obj2 = {
12 a: 3
13};
14
15var anotherFunction = HOCFunction.call(obj1);
16
17anotherFunction.call(obj2); // 3

In this case, the ReturnedFunction function is a non-arrow function, which has this reference.

Let's dig some more to explore this in arrow functions, the following example is taken from arrow function explanation in MDN

1'use strict';
2
3var obj = { // does not create a new scope
4 i: 10,
5 b: () => console.log(this.i),
6 c: function() {
7 console.log(this.i);
8 }
9}
10
11obj.b(); // prints undefined
12obj.c(); // prints 10

As we can see, obj.c has implicit binding rule applied, which points to containing object's i, whereas this in obj.b does not have this binding applied, and since there are no enclosing function scope, global scope is applied.

And that is why my friends, we often see the below line in most of the articles written about arrow functions in JavaScript.

Arrow functions does not have its own this

Closing Notes#

I hope, you got to know some rules to apply when you encounter this in your project codebases. There are a lot more about this to explain in JavaScript, which we skipped in this post just to set the basic foundation.

Just learning about this is not enough, we need to learn to use it, and use it in right sense! We need to explore some real world use cases and patterns that make use of this in JavaScript soon!

Until then, Feel free to explore the book I have attached below to take a deep dive into this JavaScript World!


References


Published:November 20, 2021

Updated:August 8, 2022