The rise of JavaScript is incredible, and might partly be explainable by its low barrier of entry. Language features like function
s and this
generally work how you might expect. To be proficient in JavaScript, you don't necessarily need to know the nitty gritty details. But eventually, every JavaScript developer WILL find a bug and realize that the cause was because this
isn’t what was expected.
Then you might start to wonder, how exactly does this
work in JavaScript? Isn’t this
something you expect to see in Object Oriented Programming (OOP)? Is JavaScript even an OOP language? If you Google that, you might find someone mentioning prototype
s, what exactly are those anyways? What did the new
keyword do before the class
keyword existed?
All these concepts are intertangled. In order to explain how this
works, I'm going to take some detours. Rather than go over a detailed enumeration of the rules of this
, I want to provide motivation for why JavaScript is the way it is.
The prototype programming paradigm in JavaScript can be used to achieve OOP. Even before you could write a class
statement in JavaScript, JavaScript was and always has been an OOP language.
JavaScript is a simple language and only uses a few moving parts to accomplish OOP principles. Some of the most important parts which I will talk about in this post are: functions
, closures
, this
, prototypes
, records
(AKA the object
literal), and new
.
Let’s create a Counter
class. A counter should have methods to reset or increment the counter. We could write something like:
function Counter(initialValue = 0) {
let _count = initialValue;
return {
reset: function() {
_count = 0;
},
next: function() {
return ++_count;
}
}
}
const myCounter = Counter();
console.log(myCounter.next()); // -> 1
We are only using functions and object literals, no this
or new
. Yet, we've already achieved some good OOP results here. We have a way to create new Counter instances. Each Counter instance has its own internal count
variable. Here we have achieved some encapsulation
and reusability
in a purely functional way!
Let's say we write a program, and that program uses a lot of counters. Every counter would have its own reset
and next
methods. (Notice that Counter().reset != Counter().reset
) Creating these closures for every method for every instance of any class in your program would require a large amount of memory usage! This isn't a case of premature optimization either, this would be so inefficient that it would not really be a viable language design. Ideally, we'd like a way to only store the properties of an instance of Counter, and share the code for the methods between all counters. (This is in essence what all OOP languages, like Java, would do.)
Without any additional language features to work with, we might end up with something like:
let Counter = {
reset: function(counter) {
counter._count = 0;
},
next: function(counter) {
return ++counter._count;
},
new: function(initialValue = 0) {
return {
_count: initialValue
}
}
}
const myCounter = Counter.new();
console.log(Counter.next(myCounter)); // -> 1
This approach solves the performance problem, but we made some pretty big compromises. Each counter instance shares an implementation, but the programmer must be painfully involved to make it all work. However, without additional language features, we might be stuck with this approach.
We can rewrite the above example to be a better experience using the this
keyword.
let Counter = {
reset: function() {
this._count = 0;
},
next: function() {
return ++this._count;
},
new: function(initialValue = 0) {
return {
_count: initialValue,
// Adds a reference to each method for each reference,
// but does not create a new function
reset: Counter.reset,
next: Counter.next
}
}
}
const myCounter = Counter.new();
myCounter.next();
// Hopefully calling reset on another instance doesn't reset myCounter
(Counter.new()).reset();
console.log(myCounter.next()); // -> 2
Notice that we are still only creating a single reset
and next
functions. (ex. Counter.new().reset == Counter.new().reset
) Notice how in the previous example we had to give the shared implementation of these methods a handle to the instance that we were calling these methods on for the program to work. Now we can just call myCounter.next()
and reference the instance with this
. But how does this work? reset
and next
are declared on the Counter
object, how does JavaScript know what this
refers to when these functions are called?
You may have noticed that functions in JavaScript have a call
method. (There's also an apply
method which is very similar, the difference here is not important.) Using call
lets you specify what the value of this
should be when calling the function. Notice how our Counter example using this
looks just like the previous example when we use call
:
const myCounter = Counter.new();
Counter.next.call(myCounter);
In fact, this is exactly what the dot notation is doing behind the scenes when it invokes the function! lhs.fn()
is evaluated as fn.call(lhs)
.
Now it should be clear, this
is a special identifier inside a function that is set when the function is called!
Let's say that you want to create a counter and increment it every second. This seems like a reasonable approach:
const myCounter = Counter.new();
setInterval(myCounter.next, 1000);
// Some time later
console.log(`Why is ${myCounter.next()} still 0??`)
Do you see the bug here? We are giving the shared next
function to the setInterval. When the interval goes off, the function is called with undefined
as this
and nothing happens. This would be identical to setInterval(Counter.next, 1000);
One common solution to this problem might be:
const myCounter = Counter.new();
setInterval(function () {
myCounter.next();
}, 1000);
We could imagine a more general way to solve this problem. Consider this approach:
function bindThis(fn, _this) {
return function(...args) {
return fn.call(_this, ...args)
};
};
const myCounter = Counter.new();
setInterval(bindThis(myCounter.next, myCounter), 1000);
Using our wrapper function, bindThis
, we can ensure that Counter.next
is always called with myCounter
as this
no matter how the new function is called! Notice that this doesn't actually modify the Counter.next
function. JavaScript has the bind functionality built in, already. The following line is identical to the example above: setInterval(myCounter.next.bind(myCounter), 1000);
Right now we have a pretty good Counter class, but it's still a little messy to write. The ugliest part might be the lines:
// ...
reset: Counter.reset,
next: Counter.next,
// ...
What we still need is a better way to share the class implementation with all instances of the class. This is the problem that prototype
s solve in JavaScript. If you try to access a property or function on the object and it does not exist, the JavaScript interpreter will try the prototype instead. You can set the prototype of an object by using Object.setPrototypeOf
. Let's rewrite our Counter class using prototypes:
let Counter = {
reset: function() {
this._count = 0;
},
next: function() {
return ++this._count;
},
new: function(initialValue = 0) {
this._count = initialValue;
}
}
function newInstanceOf(Klass, ...args) {
const instance = {};
Object.setPrototypeOf(instance, Klass);
instance.new(...args);
return instance;
};
const myCounter = newInstanceOf(Counter);
console.log(myCounter.next()); // -> 1
Our approach using setPrototypeOf
is very similar to how the new operator in JavaScript works. The biggest difference is that new
will use the prototype of the constructor function passed in. Therefore, instead of creating an object for our class methods, we just put the methods on the constructor function's prototype. Rewriting our code to use new
gets us:
function Counter(initialValue = 0) {
this._count = initialValue;
}
Counter.prototype.reset = function() {
this._count = 0;
}
Counter.prototype.next = function() {
return ++this._count;
}
const myCounter = new Counter();
console.log(`${myCounter.next()}`); // -> 1
We have finally arrived at code that you could actually see in the wild! Before the class
statement existed, this was a standard way that classes in JavaScript were written and instantiated.
Hopefully at this point it's clear why we are using the constructor function's prototype
and how this
works in the function methods. However, this still doesn't seem like a very friendly approach. Fortunately, these days there's an even better way to declare a class in JavaScript using the class
keyword:
class Counter {
reset() {
this._count = 0;
}
next() {
return ++this._count;
}
constructor(initialValue = 0) {
this._count = initialValue;
}
}
const myCounter = new Counter();
console.log(`${myCounter.next()}`); // -> 1
The class
keyword doesn't do significant magic behind the scenes. You could think of it as just syntax sugar over the previous prototype approach above. In fact, if you run the above example through a transpiler targeting ES3, you might end up with something like:
var Counter = /** @class */ (function () {
function Counter(initialValue) {
if (initialValue === void 0) { initialValue = 0; }
this._count = initialValue;
}
Counter.prototype.reset = function () {
this._count = 0;
};
Counter.prototype.next = function () {
return this._count++;
};
return Counter;
}());
var myCounter = new Counter();
console.log(myCounter.next());
Notice how the transpiler generated code that was essentially identical to the previous example!
If you've written JavaScript in the last 5 years, you might be wondering why I haven't mentioned arrow functions yet. My general advice is, "always use an arrow function unless you know you need a regular function." It just so happens to be that defining constructors and class methods is one of those specific situations where you actually need to use regular JavaScript functions. Fortunately (or unfortunately for clarity's sake), the class
keyword obfuscates that need for most people.
Some people might say that arrow functions bind the current value of this
when they are created. That is technically wrong (no this
value is bound, it is looked up via lexical scope), however, it's a good mental model to have. Every time you see an arrow function you can mentally rewrite it as:
const myArrowFunction = () => {
this.doSomething();
}
// is basically...
const _this = this;
const myRegularFunction = function() {
_this.doSomething();
}