rawjs.xyz

10.09.2023

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