‘This’ That and Dispatch
There are numerous blogs and videos about Javascript’s this
keyword. In my opinion, virtually all of them fall short and
fail to even mention dispatch
or binding
.
As always, I encourage polyglot programming. So I’ll be showing examples is Javascript, C#, Go and Rust.
Back to the basics
First, let us cover some really basic but critical Javascript.
function greet(name) {
console.log(`hello, ${name}`);
}
function greet() {
console.log(`hello, stranger`);
}
greet("natescode");
Which function is called?
If you said the first function, that is incorrect.
Javascript ONLY cares about the function name, not the parameters. In the case of duplicate function names, the last one defined wins; just like CSS.
Methods vs Functions
Now, when we get to methods, somethings change. Let us make our previous example use methods.
const Alice = {
greet: function (name) {
console.log(`hello, ${name}`);
},
};
const Bob = {
greet: function greet() {
console.log(`hello, stranger`);
},
};
var person = Alice;
person.Greet("natescode");
Now, even though Alice
and Bob
both have greet
functions, there is no longer a name conflict! Hey, I though Javascript functions had to have unique names? They do, but not methods! Because methods are functions that are related to a specific function. They have their own scope / context.
Now let’s change the example to use the object’s context.
const Alice = {
name: "Alysan",
};
const Bob = {
name: "Robert",
};
function greet() {
console.log(`hello, stranger. I am ${this.name}`);
}
// they'll both use the same function as a method
Alice.greet = greet;
Bob.greet = greet;
Alice.greet("NatesCode");
You’ll see I changed the greet
methods back to a single function. I did this to highlight the fact that methods are just functions executed within an specific object’s context. Technically, ALL javascript functions are methods since everything is on the window
object. The plain greet
function will not return null
or undefined
because the name
exists as window.name
; it is usually an empty string though.
You’ll the two lines that associate the Greet
function with both the Alice
and Bob
objects.
When we call their respecitve greet
methods, the output changes based on the object the function was called on. Methods, unlike functions, have a special parameter called the receiver. This is really obvious in Go
where the receiver parameter comes before the method name, the same order we call it in.
// 'this' could be called anything since it is just a parameter.
// normally it would be called `person` by convention
func (this Person) greet(name string){
fmt.Println("hello, " + this.name)
}
Still unclear? Oh MY!
To clear up how this
works. I show you an example in English. Here
is a script for you to read.
Hello, my name is [your name], and today . . .
Did you say “your name” or did you say your actual name? “you” and “me” and “my” are relative! They don’t refer to the same person all the time. It depends WHO says it!
Let’s change the code example to read more like English and say “my name is Alice”
const Alice = {
name: "Alysan",
greet: function () {
const my = this;
console.log(`My name is ${my.name}`);
},
};
const Bob = {
name: "Robert",
greet: function () {
const my = this;
console.log(`My name is ${my.name}`);
},
};
Alice.greet(); // My name is Alysan
Bob.greet(); // My name is Robert
So this
refers to the object that we are calling the function on, the reciever again. this
is really just a special parameter as we saw in Go
. We can think of
the following two code snippets as being conceptually equivalent.
Using this
function greet() {
console.log(`my name is ${this.name}`);
}
receiver object parameter
function greet(receiver) {
console.log(`my name is ${receiver.name}`);
}
I hope that clears up at least the basic understanding of this
.
Dispatch
Ok. So hopefully now this
is starting to make sense. We are now going to take the minecar into the cave of deeper knowledge! Let’s talk about dispatch!
First a code example. Can you tell me which method will be run?
const Alice = {
name: "Alysan",
greet: function () {
const my = this;
console.log(`My name is ${my.name}`);
},
};
const Bob = {
name: "Robert",
greet: function () {
const my = this;
console.log(`My name is ${my.name}`);
},
};
// we are picking Alice or Bob at random.
// Nothing else important about this code
var friend = Math.random() > 0.5 ? Alice : Bob;
friend.greet();
Hopefully, you answered no, I cannot predict something inherently random!
. My point exactly. This is called dynamic dispatch
or late binding
. Those are just fancy terms for Let's figure out which method to call on which object when we RUN the code, not before
In statically typed languages, we know for 100% certainty which type friend
will be, before the code runs.
C# Dispatch
Let’s translate that example into C#
class Alice {
string name = "Alysan";
public Alice(){
}
public void greet(){
const my = this;
Console.Writeline($"My name is {my.name}")
}
}
class Bob {
string name = "Robert";
public Bob(){
}
public void greet(){
const my = this;
Console.Writeline($"My name is {my.name}")
}
}
// csharp does have VAR but that just infers the type
// it isn't dynamic
var friend = Math.random() > 0.5 ? Alice : Bob;
friend.greet();
This code won’t even compile. We’ll get an `` error. Because we don’t know which type friend is going to be. The most direct translation would change the last two lines to look like this.
Using dynamic
** NEVER DO THIS **
// NEVER, EVER, EVER, for the love of keeping your job
// and not being replaced by AI, NEVER EVER DO THIS!!!!
dynamic friend = Math.random() > 0.5 ? Alice : Bob;
friend.greet();
Now friend’s type is not set until we run the code. This will work. Just don’t do it! Promise? Okay.
Using interface
// we define an interface that has a greet method
interface IGreetable{
void greet();
}
class Alice: IGreetable {
string name = "Alysan";
public Alice(){
}
public void greet(){
const my = this;
Console.Writeline($"My name is {my.name}")
}
}
class Bob: IGreetable {
string name = "Robert";
public Bob(){
}
public void greet(){
const my = this;
Console.Writeline($"My name is {my.name}")
}
}
// csharp does have VAR but that just infers the type
// it isn't dynamic
IGreetable friend = Math.random() > 0.5 ? Alice : Bob;
friend.greet();
Now, friend is of a shared type iGreetable
. An interface is a type that defines which methods should exist on an object. Now we don’t care if we get an Alice
or Bob
, we are ONLY looking for any object that fits
the IGreetable
interface, in this case that means anything with a greet
method that take no parameters and returns nothing.
NOTE: statically typed languages do differentiate between methods/functions with different number of parameters, parameter types and return types.
greet
with a return type or parameter ofstring name
would be a different method all together
Using Inheritance
// we define an interface that has a greet method
class Person {
public void greet(){
const my = this;
Console.Writeline($"My name is {my.name}")
}
}
class Alice: Person {
string name = "Alysan";
public Alice(){
super();
}
}
class Bob: Person {
string name = "Robert";
public Bob(){
super();
}
}
// csharp does have VAR but that just infers the type
// it isn't dynamic
Person friend = Math.random() > 0.5 ? Alice : Bob;
friend.greet();
Inheritance look similar to interfaces. Bob
and Alice
both inherit from the Person
object. You can think of inheritance as a compiler-assisted copy-paste. So Alice doesn’t have her own greet
method anymore but that is okay because her parent does! Similar to have Javascript’s prototypal inheritance works.
Using Sum Types
I wasn’t going to include this one, but man it is too good to pass up. I’m going to write this example in language of the gods, Rust
.
// define a Person struct with a greet method
struct Person {
name: String,
}
impl Person {
fn greet(&self) {
println!("My name is {}", self.name);
}
}
// define an enum for friend type
enum Friend {
Alice,
Bob,
}
// create Alice and Bob structs
let alice = Person { name: String::from("Alysan") };
let bob = Person { name: String::from("Robert") };
// randomly choose between Alice and Bob
let friend = if rand::random() { Friend::Alice } else { Friend::Bob };
// call greet method on chosen friend
match friend {
Friend::Alice => alice.greet(),
Friend::Bob => bob.greet(),
}
** BTW the above Rust code was generated by Chat GPT. I had it translate the previous C# code into rust but use enums for the friend type. The FUUUUTUUURE!
So let’s explain the Rust code for those unfamiliar. We define a struct
which is like a class
in many ways. It only has data though, no methods.
We then define an impl
block which just defines the methods for that struct
separately. Unlike Javascript, Rust and other languages allow us to define our own
actual types. Even if we define a class Person
in Javascript, the type will always be object
and we’d have to use the Object.prototype.isPrototypeOf()
method to see if something is from a Person
object.
The enum
in a type that combines types. So friend
can either be of type Alice
or type Bob
.
We create new object literals of type Person
for both Alice and Bob. The enum is our ticket for the train to polymophism
.
You’ll see that Rust defines an enum type which is just a type that combines two more types, type addition aka Algebraic Data Types.
Friend is of the shared type. Then at runtime, we check which variant of Friend we have and call the approprait method. This isn’t dynmic dispatch because we KNOW the type of Friend before the code runs. There is some dynamic checking however which comes with a small performance overhead. The enum is like a C union but with a bit that tells us which variant we currently have.
Effectively, enums make types into values. Like a string name
in C# must always be a string but can change to different values.
Ms. Poly Morphism
Polymorphism
is a tough term to define. I’m going to define it as the following.
Polymorphism is the process of treating code (objects, data, types) based on their shared similarities
In all the previous examples, we were simply trying to abstract
away the differences and focus on the similarites between bob
and alice
so
that we could treat them equally; even code can be inclusive!
Abstraction is another term we need to define since polymorphism is a from of abstraction. Most, if not all, definiton talk about hiding details.
I think, at least in software, it isn’t about hiding unneeded details but ignoring them. Furthermore, abstraction, is really about trying to describe the
essense of the thing that doesn’t only apply to one thing. For example, my nephew told my daughter Luna something about his iPad
and she called it a tablet
. He
corrected her that it is an iPad
. They were both correct. He was being more concrete or exact while Luna was using a more abstract, less accurate, term.
So for instance, if you know how to drive a vehicle then it doesn’t matter if the vehicle’s make is a Ford , Chevy, or Dodge because all you care is that it has two pedals (three for manual), a wheel and shifter. Everything else doesn’t matter and is not a concern.
And Beyond!
We could go further and start talking about VTABLES and more, but I think that is best saved for another blog post and video. I really hope this helps. Constructive feedback is encouraged and welcome.
Until next time
return 0;