Classes in JavaScript
Hey everyone, and welcome to my second article on prototype and classes! This week we’ll review what classes are, how to build and use them, and why you might want to use them over JavaScript’s prototype constructor functions!
As always, I’d love to hear some feedback. Comment here, or reach out to me through email, Twitter, GitHub, or my LinkedIn. As of the writing of this article I am actively looking for work. Don’t hesitate to reach out if you’d like to get in touch concerning an employment opportunity.
Prototypes Versus Classes
Before I go into explaining what classes are and how they work, you should know that JavaScript’s classes are “syntactical sugar over JavaScript’s existing prototype-based inheritance” (MDN). Whether you choose to use prototype syntax to construct your class-like systems or the newer ES6 class syntax will probably make little difference in the long run. Classes have the benefit of being similar to other languages and, in my opinion, easier to construct. Prototype constructors are pretty unique to JavaScript (which makes me wonder what other languages use it)
What Is A Class
A class is a blueprint for making objects. Classes use a function called constructor() to define variables instances of that class will have. The constructor() function runs when an instance of a class is made. Like any other function it can hold logic, so if you need something done every time an instance of a class is made, this is where you’d put that logic (or better yet, call the function that contains that logic). Class functions, prototype functions, static variables, instance variables, and more can all be defined inside the class definition as well. Let’s look at an example of an Animal class:
There is quite a lot going on in the above example, so let’s unpack it bit by bit:
- On line 1 we define our class just like you see: class Animal {…
- On line 3 we define a static property for our class. This property is only accessible through this class with Animal.listOfAnimals.
- On line 6 we call the constructor() function, a prototype function. All instances of classes run this function upon their creation. It can take arguments, just like the prototype constructor, and perform operations with those arguments. Here, it is setting the new instance’s name property (line 8), and pushing that instance into the Animal class’ Animal.listOfAnimals array property (line 10), then logging Animal.listOfAnimals so we can make sure it was pushed there (line 12).
- Line 16 shows that classes can have their own functions, called static functions, which are only callable on that class (like how the class only has access to static properties). Calling this static function must be done with Animal.className(), as seen on line 31.
- Line 25 shows how to assign prototype functions to classes. These are the same as we saw in last week’s article, as function.prototype.myFunction(), which will be inherited by instances of this class (or in that case, instances of that constructor function). Prototype functions can be added to classes after their creation with Animal.prototype.functionName, just like with prototype constructors.
- On line 35 we make a new instance of our class with new Animal(‘fish’).
- On line 36 we test that the prototype function is working.
You may notice that we use this in two different ways in our example. When used inside static functions and properties, this refers to the class itself. Hence why on line 31 the string “Animal” will be returned (all classes have the name property). In prototype functions, this refers to the instance being created, so our constructor() function and prototype functions, just like in prototype constructor functions, use this to refer to the new object.
Classes in JavaScript are not hoisted, meaning they must be declared before they are used.
Extending Classes and Super
Classes can be chained together just like objects in the prototype chain. Extending classes into new classes allows you to take properties and functions from the class being extended, and expand upon that class in a new class.
For instance, let’s extend our Animal class into a Mammal class:
This should look similar to you to our Animal class example but with a large exception: super(). Using super(), we can access and make additions to properties of the class our new class is inheriting from. Here, we use super to have our Mammal class take the Animal class’s name parameter in addition to the hasHair parameter. If we plan on expanding the constructor function or any other function derived from a previous class, we must call super beforehand. And, if we plan to use this in our new class, we must also call super() beforehand.
If this is confusing, don’t worry: the concept can be a bit hard to wrap your head around at first. But let me try explaining the way I think about it:
Our child class, Mammal, wants to have the same parameters its parent class, Animal, has — the name parameter. So in our Mammal class’s constructor we list name as a parameter to the constructor() function as well as any other parameters we want, in this case hasHair. Then, inside our constructor() function, we call super() and feed it the name parameter as an argument, before using the this keyword! That will run the Animal class’s constructor() function before running the Mammal class’s constructor() function. After that function runs, our Mammal class’s constructor() function will run, and when we generate an instance of our Mammal class it will have both name and hasHair properties!
Let’s run the code above and make a new instance of a Mammal:
Here we see that the Animal class’s constructor function runs and logs the contents of Animal.listOfAnimals when myFish is created, and again when human is created. We can see that both instances of both classes exist inside Animal.listOfAnimals. And we know the Mammal constructor() function ran after the Animal constructor() function because it logged the new instance of Mammal, human, from line 46 of its constructor() function.
We’re also calling super inside of the mammalName() prototype function on line 49. Here, we’re adding onto the Animal class’s prototype function instanceName() by also calling a console log on line 50. So just like calling super() inside our constructor() function we can also call super() inside child class prototype functions.
Decisions Decisions: Prototype Constructor Functions Vs Classes
Okay, here it is, the answer to the question on everyone’s mind: “Should I be using prototype constructors or classes? Which one is better?”.
This question is kind of tough to answer. I think the best answer comes from AirBnB’s style guide, which reasons that the class syntax is easier to read, and that’s a good enough answer for me. Having readable code is hugely important when working on a team and might make up for inefficiencies in that way your application runs. At the end of the day different developers prefer different practices, and it’s most important to stay flexible and use the practices your team prefers.
As far as the efficiency of each, I’m not comfortable making a claim either way. As far as I can tell from reading various articles, classes and prototype constructors are different, but I can’t prove that. Again, I suggest opting for using classes over prototype constructors, but ultimately that’s up to you!
Well, that’s it for this week. I hope this shed some light on how classes work, and showed that they’re easier to work with than prototype constructors. There is so, so much more to cover on this topic, and I hope this article sparked some interest in you in finding out more. If you have comments, questions, corrections, or a job opportunity, let me know using the links below.
Further Reading