[Typescript OOP 2/3] Using "this"
This is the 2nd of a 3-part series where I explain function bindings, scope of "this" and the benefits of arrow functions.
this
is perhaps the most used keyword in Object Oriented Programming (OOP) model. You see, this model is based on the premise that each and everything we do on our code can be classified as objects having custom properties of their own. In fact, even the "main" or entry function of C++ and Java is also a class called main. The compiler looks for this class and starts to find its links and imports there on.
This programming style considers every aspect of code as an object which might consist of plenty complex methods that interact with others on a very superficial level without giving out any internal working. Whenever those internal workings need to refer to itself, they use the keyword "this".
this, self, Me is the way of pointing to the current class of the object/class we are working in. Since methods of same name could be part of different classes, "this" keyword is used to tell the compiler that we are interested in calling our current class methods and properties.
There are many explanations to OOP but the best I could ever find is that of Steve Jobs to a Rolling Stone interview.
Would you explain, in simple terms, exactly what object-oriented software is?
Steve Jobs: Objects are like people. They’re living, breathing things that have knowledge inside them about how to do things and have memory inside them so they can remember things. And rather than interacting with them at a very low level, you interact with them at a very high level of abstraction, like we’re doing right here.
Here’s an example: If I’m your laundry object, you can give me your dirty clothes and send me a message that says, “Can you get my clothes laundered, please.” I happen to know where the best laundry place in San Francisco is. And I speak English, and I have dollars in my pockets. So I go out and hail a taxicab and tell the driver to take me to this place in San Francisco. I go get your clothes laundered, I jump back in the cab, I get back here. I give you your clean clothes and say, “Here are your clean clothes.”
You have no idea how I did that. You have no knowledge of the laundry place. Maybe you speak French, and you can’t even hail a taxi. You can’t pay for one, you don’t have dollars in your pocket. Yet I knew how to do all of that. And you didn’t have to know any of it. All that complexity was hidden inside of me, and we were able to interact at a very high level of abstraction. That’s what objects are. They encapsulate complexity, and the interfaces to that complexity are high level.
this in Typescript
As we learnt in my previous post, Typescript is Javascript with an OOP layer over it. Hence, Typescript developers too need to familiarize themselves with it.
From the code that we wrote before:
class Person
{
/** age of the person */
private age: number; // Declaration only. Initialized in the constructor. Hence, type is required now.
/** name of the person */
private name: string; // Declaration only. Initialized in the constructor. Hence, type is required now.
/** Prevent any modifications boolean */
isReadonly = false;
/** Constructor: Initialization of properties */
constructor(name: string, age: number)
{
this.age = age;
this.name = name;
}
/** greet message for the person */
private getGreetMessage(): string
{
return `Hello world! My name is ${this.name} and I am ${this.age} years old.`
}
/** method to greet others */
greet(): void
{
console.log(this.getGreetMessage())
}
/** sets age */
setAge(newAge: number): void
{
if (this.isReadonly)
{
console.warn("Attempt to set age when it is set to readonly")
return;
}
this.age = newAge;
}
}
Observe that we used the keyword this to access private properties like age, name, and public property isReadonly. We also used this to access our methods such as getGreetMessage().
Deviating scope of this
Let's look at this simple example below:
class Foo {
// private member bar which holds value 42.
private bar = 42;
// public method qux that returns the value of private member bar
qux(): number {
return this.bar;
}
}
const baz = new Foo();
console.log(baz.qux());
// expected output: 42
Foo is a class with private member bar that holds value 42. Foo also has a public method qux that returns value bar of this. Now for most cases, this is perfectly fine as we are dealing with one class.
However, look at the extension below:
class Foo {
// private member bar which holds value 42.
private bar = 42;
// public method qux that returns the value of private member bar
qux(): number {
return this.bar;
}
// A public object thud that has property bar and method qux2.
thud = {
bar: 37,
fred: this.qux
}
}
const baz = new Foo();
console.log(baz.thud.fred());
// expected output: 37
We added a new public object thud inside of our class Foo. We added members bar and a method fred that should return the value of Foo's qux method.
Did you notice that qux returned the value of bar inside of the object thud and not the value of bar where qux was actually defined? It is because this has a varying scope. It does not matter where the method containing the reference to this was defined. What matters is this automatically starts refering to the object that calls it. Hence, calling fred inside of thud does indeed call the member qux which further proceeds to get "this.bar" which in the case of object thud, is 37.
Function.bind
One way of defining the scope of this is to use the prototype method bind to our method before calling it.
// Instead of calling
baz.thud.fred();
// expected output: 37
// We call
baz.thud.fred.bind(baz)()
// expected output: 42
Notice how calling bind forces the method qux to use the object baz: Foo for the call of this instead of the object thud. Hence, the moment chain our method fred with the call to bind and passed the baz object, the object so far has definite scope of using baz as its this. Now we need to call the method which we do by the braces ().
If our aim is to just call the function with our defined scope, we can do it this way too:
baz.thud.fred.bind(baz)();
// IS EQUIVALENT TO
baz.thud.fred.call(baz);
// expected output in both cases: 42
In order to strictly call the qux method for thud object, we can pass the object thud to call or bind methods like below:
baz.thud.fred.bind(baz.thud)();
// IS EQUIVALENT TO
baz.thud.fred.call(baz.thud);
// expected output in both cases: 37
Using prototype functions bind and call are ways of defining the references that this should anchor to. Otherwise, this takes the scope of whichever object that calls it.
Arrow Function
We introduced arrow function in our Promise Chaining blog post where we learnt that single-line arrow functions have implicit returns.
However, there is more to the specialty of arrow functions.
Observer how we can rewrite our class definition with qux as arrow functon
class Foo {
// private member bar which holds value 42.
private bar = 42;
// public method qux that returns the value of private member bar
qux = (): number => this.bar;
// A public object thud that has property bar and method qux2.
thud = {
bar: 37,
fred: this.qux
}
}
const baz = new Foo();
console.log(baz.thud.fred());
console.log(baz.thud.fred.bind(baz)());
console.log(baz.thud.fred.call(baz));
console.log(baz.thud.fred.bind(baz.thud)());
console.log(baz.thud.fred.call(baz.thud));
// expected output: 42
// expected output: 42
// expected output: 42
// expected output: 42
// expected output: 42
Notice that our value for the method qux is now defined by this as its parent class Foo and it does not matter which object calls it. It is because arrow functions do not create their own classes and do not have their own implicit constructors. They are just like values that are generated then and there. Hence, even the ordering of the methods matter in this case. If we put qux after thud, we might have a problem. Our compiler will complain about lack of initialization before it is called.
However, the compiler will be completely fine with this order if qux was a normal function.
So, we learned in this post that the reference of this depends on which object calls it in runtime. And there, these may be difficult to catch during compilation as it is a design flaw instead of the code problem because it makes full sense in the way you wrote it but it might not be what you intended.
Next, we are going to learn about Interfaces, Enums and Object operators on Typescript.
Thank You! 😄