What is this? Javascript?
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 scope45 console.log( "parent" );6 child(); // <-- call-site for `child`7}89function child() {10 // call-stack is: `parent` -> `child`11 // so, our call-site is in `parent`1213 console.log( "child" );14 grandchild(); // <-- call-site for `grandchild`15}1617function grandchild() {18 // call-stack is: `parent` -> `child` -> `grandchild`19 // so, our call-site is in `child`2021 console.log( "grandchild" );22}2324parent(); // <-- 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 parent
→ child
→ grandchild
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}45var a = 2;67foo(); // 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 returnundefined
instead of2
.
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}45var obj = {6 a: 2,7 foo: foo8};910obj.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 obj5 }6};78obj.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}45var last = {6 a: 42,7 foo: foo8};910var first = {11 a: 2,12 last: last13};1415first.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}45var obj = {6 a: 27};89foo.call(obj); // 21011var obj2 = {12 a: 5513};1415foo.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// CALL2function foo(arg1, arg2) {3 console.log(this.a + arg1 + arg2);4}56var obj = {7 a: 28};910foo.call(obj, 2, 2); // 6111213// APPLY14function foo(...arr) {15 console.log(this.a + arr[0] + arr[1]);16}1718var obj = {19 a: 220};2122foo.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 class
es 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 }56 tellName(){7 console.log('Your name is ' + this.name);8 }9}1011const obj = new NameBuilder('Bharathi');1213obj.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}78const obj = new NameBuilder('Bharathi');910obj.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.
- A Brand new object is created (aka, constructed) out of thin air
- the newly constructed object is [[Prototype]]-linked
- the newly constructed object is set as the
this
binding for that function call - 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');23// 1. A Brand **new object** is created (aka, constructed) out of thin air4// creates new object {}56// Lets skip #2 for now78// 3. the newly constructed object is set as the `this` binding9// for that function call10// When the below function is called, `this` points to {}11// Since {} has no `name` or `tellName` properties, they would be set1213function NameBuilder(name) {14 this.name = name;15 this.tellName = function tellName(){16 console.log('Your name is ' + this.name)17 }18}192021// 4. unless the function returns its own alternate object,22// the new-invoked function call will automatically return the23// newly constructed object.24// Our function does not return anything, hence, the newly25// created object with two properties, is returned2627const obj = new NameBuilder('Bharathi');2829// Becomes:3031const obj = {32 name: 'Bharathi',33 tellName : tellName() {34 console.log('Your name is ' + this.name)35 }36}3738// So, now when we execute `obj.tellName()` which has `this.name`39//would point to obj.name,4041"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 }67 return {8 name: "Rajaraam",9 tellName: function tellName() {10 console.log("Your name is " + this.name)11 }12 }13}141516const obj = new NameBuilder('Bharathi');1718obj.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.
- Is the function is called with
new
thenthis
would be newly constructed object.
var bar = new foo();
- 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);
- Is the function executed with containing object? Then
this
would be that object.
var bar = obj1.foo();
- 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 function3 return () => {4 // `this` here is lexically adopted from `foo()`5 console.log(this.a);6 };7}89var obj1 = {10 a: 211};1213var obj2 = {14 a: 315};1617var anotherFunction = HOCFunction.call(obj1);1819anotherFunction.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}67var obj1 = {8 a: 29};1011var obj2 = {12 a: 313};1415var anotherFunction = HOCFunction.call(obj1);1617anotherFunction.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';23var obj = { // does not create a new scope4 i: 10,5 b: () => console.log(this.i),6 c: function() {7 console.log(this.i);8 }9}1011obj.b(); // prints undefined12obj.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