In my last article, I talked about how to implement the new
keyword from scratch. Now we are going to take it a step further and re-implement a basic version of the somewhat controversial class syntax introduced in JavaScript. Why is it controversial, you might ask?
class Rectangle {
constructor(width, height) {
this.width = width
this.height = height
}
size() {
console.log(`${this.width} x ${this.height}`)
}
}
class Square extends Rectangle {
constructor(scale, width, height) {
super(width, height)
this.scale = scale
}
size() {
console.log(`${this.width}px wide and ${this.height}px tall`)
}
}
Looks pretty straightforward, right? I agree. However, there is one thing wrong with all of this...
The Problem with JS Classes
The problem with classes is that... well... JavaScript doesn't have classes! It's a language based on prototypal inheritance , not classical inheritance.
It's like trying to put a dress on a bear. Sure it will look less scary, but it doesn't change what's under the dress.
Don't be the guy or gal who uses classes thinking it works exactly like classes in Java or Python. Impress your hiring interviewer by understanding what's underneath! JavaScript classes I mean, not the bear with the dress.
Steps to implement classes in JavaScript
Let's implement our example above in plain old JavaScript without the syntax sugar. Here's what we will have to do:
- Create our
Rectangle
constructor function withwidth
,height
, andsize
attached tothis
- Create a second constructor function called
Square
, which will call ourRectangle
function to initialize parameters (this is thesuper(...params)
line). It will also have a new member calledscale
- Make sure our
Square
"class" inherits the prototype ofRectangle
as well by copying the prototype ofRectangle
- Copying the prototype of
Rectangle
meansSquare.prototype.constructor
will beRectangle.prototype.constructor
rather than the Square constructor we defined, so we must redefine the property.
Does all that make absolutely zero sense? No problem, let's go step by step with code.
Step 1
Create our Rectangle
constructor function with petName
and bark
attached to this
Easy enough:
function Rectangle(width, height) {
this.width = width
this.height = height
this.size = function() {
console.log(`${this.width} x ${this.height}`)
}
}
Nothing new here, just a standard constructor function as we would do pre-class syntax days.
Step 2
Create a second constructor function called Square
, which will call our Rectangle
function to initialize parameters (this is the super(...params)
line).
function Square(scale, width, height) {
Rectangle.call(this, width, height)
this.scale = scale
}
This is where the confusion often begins. Why did we call Rectangle.call(this, width, height)
? This basically says "call our Rectangle constructor function, but use the this
parameter we pass in rather than the one in Rectangle. Also, pass in any other parameters expected by Rectangle." This is essentially the same is running super(width, height)
.
Our other member, scale
, is exclusive to our Square class, so we assign it after we run the parent constructor function.
Step 3
Make sure our Square
"class" inherits the prototype of Rectangle
as well by copying the prototype of Rectangle
Square.prototype = Object.create(Rectangle.prototype)
What the hell is this? Great question!
In plain english, this basically says "I want the prototype of Square to be a copy of the prototype of Rectangle".
Okay, so you may be wondering now, why do we want to do this? Take the following example:
Rectangle.prototype.getArea = function() {
return this.width * this.height
}
If we define the getArea
method on the prototype of Rectangle
, but forget to do step #3, our Square
won't have access to this method. Why would we define methods on prototypes? You'll have to follow me and wait for the next article to explain that one!
Step 4
Copying the prototype of Rectangle
means Square.prototype.constructor
will be Rectangle.prototype.constructor
rather than the Square constructor we defined, so we must redefine the property.
Our last step is an odd one, but basically if ran:
Square.prototype.constructor.name === Rectangle.prototype.constructor.name
we would see that they are equal, which isn't what we want. We want our Square
to point to the Square
constructor function, but because we literally copied the whole Rectangle
prototype, we lost that connection.
So let's fix that:
Object.defineProperty(Square.prototype, 'constructor', {
value: Rectangle,
enumerable: false, // prevents this property from showing up for-in loop statements
})
Step 5: Profit
Phew! That wasn't super straightforward. Here is our final implementation:
function Rectangle(width, height) {
this.width = width
this.height = height
this.size = function() {
console.log(`${this.width} x ${this.height}`)
}
}
function Square(scale, width, height) {
Rectangle.call(this, width, height)
this.scale = scale
}
Square.prototype = Object.create(Rectangle.prototype)
Object.defineProperty(Square.prototype, 'constructor', {
value: Rectangle,
enumerable: false,
})
You might be thinking, "okay... nope I'm just going to use the class syntax", which is exactly why it was introduced in the first place!
The point of this article isn't to say "hey, classes aren't real so don't use them". The point is to understand what is really happening behind the scenes so you can make the educated decision between using class syntax or functions.
Coming Up Next on December 2nd...
We saw how classes are implemented in plain JavaScript, but what are the pros and cons of using the class syntax? Are there performance implications?
Follow me to find out! Or you can just google it I guess...