JS 101: Implementing Classes From Scratch

JS 101: Implementing Classes From Scratch

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:

  1. Create our Rectangle constructor function with width, height, and size attached to this
  2. Create a second constructor function called Square, which will call our Rectangle function to initialize parameters (this is the super(...params) line). It will also have a new member called scale
  3. Make sure our Square "class" inherits the prototype of Rectangle as well by copying the prototype of Rectangle
  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.

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...