The problematic `this`
The simplest way to explain how this
works is by thinking of it as an object
that provides context to a scope.
To understand what I mean, we'll try to replicate what this
does with simpler
language constructs.
Note: The Javascript language keyword
this
is represented in quotes throughout the post to avoid confusion with it's english language counterpart
1const obj = {
2 v: 1,
3};
4
5function a() {
6 console.log(obj.v);
7}
Since, the scope of obj
is the entire file; you can now access this obj
in
the function a
without any issue. If we change it to a little more contained
function then it'd look something like this
1function a() {
2 const obj = {
3 v: 1,
4 };
5
6 console.log(obj.v); //=> 1
7}
8
9console.log(obj.v); //=> errors out
Here, the 2nd console.log
basically has no access to what's inside the
functions scope. If it's all clear till this point, we can move forward to
understanding the this
keyword
To make it very simple, the this
keyword refers to the current scope, since JS
works with objects as the basic building block of everything, you can refer to
anything that's inside an object with the this
variable.
1function a() {
2 console.log(this); // undefined
3}
4
5const obj = {
6 value: 1,
7 method: function () {
8 console.log(this.value); //=> 1
9 },
10};
An isolated function is always initialized with an undefined
this
and this
is almost always true, but then when you define this function inside an existing
object, the function is now defined in an inherited context. You can also do
this manually by doing so.
1// Longer variation
2const obj = {
3 value: 1,
4};
5
6function methodWithoutThis() {
7 console.log(this.value);
8}
9
10obj.method = methodWithoutThis.bind(obj);
11
12methodWithoutThis(); //=>errors out
13
14obj.method(); //=>1
The bind
function is a helper construct provided by JS to be able to assign
the value of this
to other JS Object constructors (arrays, objects, functions,
etc)
This is a little more verbose but basically you now have a separate object, a
separate function with no definition of this
and then you tell JS that you
wish to bind methodWithoutThis
with a custom context, which in this example is
called obj
.
Context Providers
Where else do you think the context for this
is thrown down into a function?
Here's the answer. Everywhere the function belongs to an existing instance of an object constructor.
1const obj = {
2 value: 1,
3 method() {
4 // belongs to `obj` so `this` contains the data of `obj`
5 this.value; //=>1
6 },
7};
8// or let's write the longer form
9
10const obj = {
11 value: 1,
12};
13obj.method = function () {
14 // belongs to `obj` so `this` contains the data of `obj`
15 this.value; //=>1
16};
17
18const arr = [1, 2, 3, 4];
19arr.anotherLengthFunction = function () {
20 // now belongs to the arr instance
21 console.log(this.length); //=>4
22};
23
24function FuncWithConstructor(count) {
25 this.value = count;
26
27 this.method = function () {
28 // pretty obvious in this one.
29 console.log(this.value);
30 };
31}
32
33// but there's a twist in the usage of the function here.
34const func = new FuncWithConstructor(1);
35func.method(); //=>1
36
37class ClassDef {
38 constructor(value) {
39 this.value = value;
40 }
41
42 method() {
43 console.log(this.value);
44 }
45}
46
47const cls = new ClassDef(1);
48cls.method(); //=>1
Most of them now make sense, right? Though there's one tiny hiccup here. The
function definition of FuncWithConstructor
's usage is different as compared to
how you would normally use it.
This is what we call a Contructor Functions or more commonly known as Object Constructors. This is the basic building block of the entire language other than a few primitives, also why the language is so easy to extend.
Let's try to understand as to why the new
is required in that scenario to
work. Remember I said that a function is always initialised with undefined
.
When you call a function using Function()
, the value of this
is pretty much
undefined
, you can log it to see that but, when it's initialized using new
JS knows that this is an object constructor and that the this
variable is
supposed to be an empty object
A verbose version of that would look like below.
1function A() {
2 this.value = 1;
3}
4
5function newManual(fn) {
6 const a = {};
7 fn.apply(a);
8 return a;
9}
10
11const a = newManual(A);
12console.log(a.value); //=>1
Though this is handled in the JS VM as syntax instead of you having to use a
helper like the the one we create (newManual
), in which the variable a
is
the base of this
.
The new addition here is the apply
helper called on fn
. The apply
function
itself does the same thing that bind
would do but instead of just binding the
this
value, apply
will also call the function for you.
If you wish to make it more verbose, you can do the same with bind
as well.
1function newManual(fn) {
2 const a = {};
3 const exec = fn.bind(a);
4 exec();
5 return a;
6}
Ways to persist and pass down context
1function A() {
2 this.value = 1;
3
4 this.method = function () {
5 console.log(this.value);
6 };
7}
8
9function logA() {
10 return new A().method;
11}
12
13const g = logA();
14g(); //=>undefined instead of 1
Well, that's awkward, looks pretty much as valid code but there's a tiny hole
here. The variable g
is calling an isolated function logA
which creates an
instance of A
and returns a function def of method
, the returning of
method
is where we screw this up.
When method
is being accessed and returned, it's own context at that point is
similar to defining a function
inside logA
which because of being isolated
has no this
.
1function A() {
2 this.value = 1;
3
4 this.method = function () {
5 console.log(this.value);
6 };
7}
8
9function logA() {
10 // the modification
11 const ctx = this || {};
12 ctx.value = 2;
13
14 return new A().method;
15}
16
17const g = logA();
18g(); //=> 2 instead of 1
It now takes in the original context passed to it by the calling function, which
in this case is logA
so if I had to keep the original context of the instance
getting created, I would have to be careful and make it without exposing the
method or pass the instance with bound methods.
1function A() {
2 this.value = 1;
3
4 this.method = function () {
5 console.log(this.value);
6 };
7}
8
9function logA() {
10 const inst = new A();
11 return inst.method.bind(inst);
12}
13
14const g = logA();
15g(); //=> 1
Though you can probably make it easier by not exposing single methods or selectively exposing the function constructor. A similar problem for this can be created using destructured variables as well.
1class A {
2 constructor() {
3 this.value = 1;
4 }
5
6 method() {
7 console.log(this.value);
8 }
9}
10
11const instance = new A();
12
13instance.method(); //=>1
14
15const { method } = instance;
16
17method(); //=>TypeError: Cannot read properties of undefined (reading 'value')
This can be very easily solved by manuall binding during declaration but this is also something that's being changed because of the change in parent during the initiation of the variable.
1class A {
2 constructor() {
3 this.value = 1;
4 this.method = this.method.bind(this); //=> fix: manually bind it to the instance
5 }
6
7 method() {
8 console.log(this.value);
9 }
10}
11
12const instance = new A();
13
14instance.method(); //=>1
15
16const { method } = instance;
17
18method(); //=>1
These are some quirks of the this
keyword and also why people run away from it
but if you understand the basic principle of how the scope differs during each
init stage, it's a little less surprising