JavaScript, compared to many other programming languages is a prototype-based language. The creation and behavior of objects are based on prototypes that are objects themselves. Compared to other class-based languages, JavaScript doesn't have any class system and relies on objects. With the addition of classes in JavaScript, it's now possible to use more traditional class creation and inheritance. Classes are templates for objects. You create a single template which is used to create various objects.
Before reading further, I recommend you get familiar with objects as well as functions first. It will make this reading much easier.
Table of Contents:
- How to create JavaScript classes?
- Class declaration
- Class expression
- Class methods
- Fields
- Getters and Setters
- Inheritance
- The super keyword
- Inheriting fields
- Evaluation order
- The instanceof operator
- Conclusion
How to create JavaScript classes?
We can define classes in two ways:
- Class declaration
- Class expression
Class declaration
To declare a class we use a keyword class, name this class starting with an uppercase letter, and create its body with curly braces.
This way we declare a class that will be a template for our future objects. Separately, it is not going to do anything right now, it's useless until we use this template to create objects from it.
But before we do that, let's quickly examine it in the console.
In the console, it shows the whole class, a reference to the class. It's not finished yet but can you guess the type of the class? Is it an object considering that it's a template for objects?
To check the data type we can use a typeof operator.
The type of a class is a function. Why? Because behind the scenes, classes are built with constructor functions. Constructor functions in JavaScript are functions that create objects. Constructor functions are also skeletons, and templates, which will later be used to create instances (objects) that inherit everything from this constructor.
Make sure to read the constructor functions sections in my post about functions in JavaScript.
Once we create the user class, we will be using it as a template for future users who will register on the website, for example. We will have many users and we need to think, what would these users have in common? A username and an email, right? Also a password but let's skip the password for now. For every user that registers, we want them to have a username and an email.
Imagine we don't have any options and we need to create a new object from scratch every time someone registers. This is how it would look like if I had to manually create every object when someone registers.
I created three various users manually. But if I had to do that for a thousand users? Or million?
All these users have a username and an email. The only difference is the value. Instead of repeating it many times, we can create a template, which has the username and email. What if we were able to create users with fields like user and email all the time, without the need to repeat all the code?
To achieve this goal, we can add properties, a data in the form of key/value pairs. In our case, we can add a username and an email property. This can be done with a method constructor.
Class constructor
A constructor inside a class is a special method where you can create initial values. These values will be used in the future objects that we create with the help of this class. The objects we will create are called instances. The constructor can have parameters, just like functions, and the values of these parameters will be saved in the corresponding key.
In the example above, whatever argument we pass to the username, will be a value of the key username. Same for the email.
The keyword "this" that we use, refers to the instance created from this class and helps to understand which instance the values are coming from.
You can create only one constructor per class, otherwise, it will throw an error.
Let's summarize the information above and add a few more details to it:
- To create a class we need to use the keyword "class" along with the class name.
- Class names should start with an uppercase letter. It's not mandatory, it's a convention that helps us to differentiate classes from other objects or functions.
- Classes are not objects, they are functions, and templates that create an object.
- To create properties and potential key/value pairs, we need to use a method constructor. A method is the same as a function but methods are not functions because they are attached to other functions or objects. Constructors are not mandatory but we usually need them as we create default data.
- Property names should be descriptive and indicate what the possible value can be. If you expect an email, you would name a key an email, not something like "valueOne", for example.
Creating instances
We created a constructor which means we can add users now. Remember how we created 3 users? Let's create them again with the help of our class.
We are going to create class instances (objects) with the keyword new and the class name while saving it in a variable. When you create an instance, the constructor method is called and accepts the arguments you have passed.
This code looks so much cleaner and easier compared to the previous way of creating users.
Let's inspect these users in more detail to see what they contain.
Each user has the key/value pair of a username and an email, as expected. As you can see, if we expand the first user, it also contains some [[Prototype]].
As we mentioned earlier, the instances of the class that we create are objects. The class is a template to create objects and the instance of the class will be an object.
We can check this with the help of the typeof operator.
Each object in JavaScript comes along with a property called Prototype which is an object and which also has its prototype. This is called a prototype chain. Every prototype will have its own prototype until it reaches null. Null doesn't have its prototype and this is when the prototype chain ends.
So the useOne prototype chain would be the prototype of the user we created that inherits properties from the user class, then the prototype of the user class where there are inherited properties from the built-in object which then leads to null.
Class expression
Instead of creating a class declaration, a named class, you can also create a class expression. A class expression allows us to create a class without naming it directly. The class expression cannot be created if it starts with the keywords class. That's why we need to save the class expression inside a variable and refer to it later.
Let's re-create our previous class of the user. Even if we are saving it in a variable, it is recommended to start the variable name with an uppercase letter so we can differentiate it.
We can also create a named class expression but the name will be available only inside of the class.
Class methods
As I mentioned earlier, methods are functions that belong to the objects, they don't exist on their own. The constructor is a built-in method that already exists automatically however we can also add any other methods on our own. If we want to create a method that will be inherited by every new user, we need to create it inside the class body.
The methods you write can be various. You can write a casual function, an async function, and even a generator function.
Let's inspect our user and find the method.
We cannot see the sayHi method right now because we need to search for it in the second prototype, this is where the inherited things are located.
The user inherited this method and we can use it. To call the methods, we need to use the class instance name and call the function with the dot notation.
As a result, we can see "Hello there" in the console. We didn't have to re-create the method for a new user. Every new user will have this method.
We can also create a method for this instance only but it will not be shared across other users anymore. That's why usually it's not recommended as it doesn't align with object-oriented programming unless you have a specific usage for this target object.
As you see in the example above, if we create a method for one instance it does work but it will not be shared across other users and it's not something we need.
In classes we can differentiate several types of methods:
- Instance methods
- Static methods
- Private methods
Instance methods
Instance methods are the methods we have covered earlier. They work with instance-specific data, are defined in the class prototype, and are inherited by all instances.
Static methods
Compared to instance methods, static methods are not inherited and belong only to the class. To define a static method you simply need to use a keyword static in front of the function.
Let's convert our previous method sayHi() into the static method and see what happens.
We converted our instance method to a static one and when we try to call this method from the instance, it throws an error. Why? Because this method is static now and is only available for the class, it's not shared across instances of this class.
To use this static method we need to call it directly from the class.
This time, it works and logs the results successfully.
Why do we need static methods?
Static methods are used in scenarios when you don't need to do anything with the values of the instances but you do need to have some values saved in the class. For example, some mutual information is not about specific instances but general ones.
Even though they are not mandatory, of course, static methods are a great way to encapsulate functionality in one place, not pollute a global scope, and make the code more organized, reusable, and easier to debug. Such functions are often called utility functions, operations, or helper methods.
There are several scenarios where we can use static methods:
Handling date and time: We can add various date or time-related functions that parse dates, format or calculate date or time.
HTTP requests: You can implement GET or POST requests inside classes and make them reusable while using various endpoints.
Mathematical calculations: Another situation where we can implement static methods are math operations like add, multiply, divide, and so on.
General purpose: Often mentioned as utility functions, you can add static methods for general purposes like conversion to string, some form validations, number conversion, and so on.
Private methods
You can also create private methods that will remain private for this class. Private methods are available for classes only. Before the existence of classes, making things "private" was possible with the help of closures.
To create a private method, you simply start the method name with a hash (#). Let's re-create our class one more time.
In the example above, we created a private method however compared to a static method, you can notice that we cannot use it not only in the instance of this class but we cannot even use it in the class itself. Why do we need them then?
Private methods can be useful when we want to change something internally, inside the class, and have it fully hidden. One of the usages can be situations when we work with some data we do not want to exposeā-āpasswords or any personal information, or any data validation logic that you don't want to expose. You can also write some type of authentication methods, that you don't want to be exposed, some payment operations, some API requests you want to keep private, and so on. Private methods still work inside the class itself however you need to use additional logic to make use of them.
Let's see how we make use of private methods. Here is a very primitive example:
In this example, we create a private method #saySomething() however to use it we call it inside the response method.
The main idea is that whatever happens inside the saySomething will not be exposed and cannot be affected or reached anywhere.
Fields
Fields is a concept introduced for classes that are very similar to properties and methods. In many sources, both fields and properties are used interchangeably.
Considering that classes appeared in JavaScript a little later, the concept of fields comes along with classes in an attempt to switch from prototyped-based programming to object-oriented programming.
Classes are often mentioned as a syntactic sugar, which means that the way code is written and read, is more similar to object-oriented programming but the actual functionality behind the scenes doesn't change. The way you write code creates an expectation that it has a new functionality but behind the scenes, functionality does not change.
Let's consider an example of fields from other programming languages.
For example, in C# properties expose fields while fields are meant to be private to the class and accessible via getter or setter methods. Fields are not supposed to expose how the class works internally. Properties however are a way to access these fields and do some actions with them, at the same time not exposing the implementations.
When classes were introduced, we could create property values in constructors, just like we did earlier:
In order to simulate object-oriented programming, we also need some privacy for properties, right? To achieve that, the option was to use a convention to add an underscore before the property to indicate that this is supposed to be private. This means that it wasn't some feature of JavaScript and didn't have actual functionality. It was just "an agreement" between developers that underscore (_) means it's supposed to be private.
Let's illustrate how the private property of the password should be added:
However, if you create a new user and try to access this property, you will be able to access it anyway. So it's not really private. Such properties are often mentioned as pseudo-private properties.
Next, there was a proposal to add fields inside of the class body. This would make fields different from regular properties that we know of. Let's move our password to a class body. You can initialize a field with a value right away but it's not mandatory. I will make a password empty string.
If we try to access the password field, it will be available again but into play comes a private field, and this time it's not a convention with underscore.
Private fields
To create a private field, we use a hash(#) before the name of the field.
What if we try to access the password along with the hash?
We cannot access it anyway. Now this sounds more like fields we needed, just like in object-oriented programming. The private field becomes a little harder to access, kind of.
In order to access the password outside the class you can use methods.
You can also simply pass them to the constructor but then you are just creating a regular property so instead maybe it's better to pass an updated value.
Obviously, one should not expose passwords like this and it's just an example. Usually, if there is a possibility or even a need to expose some sensitive data, the methods are not as simple as in the example above. Usually, such exposure includes additional authorization, authentication, and access control logic before one can access private fields that easily.
Static fields
Remember the static methods we mentioned earlier? Besides the methods, you can also create static fields. They serve a similar purpose and are used to store the information inside the class itself. They are shared across instances of the class but it's static data that can be accessed without the need to create an instance.
For example, let's create a class for an online store product. In this store, I want to monitor all available products.
Every time there is a new product, I will create an instance of the product but inside the class itself, I will also add a static field for product quantity.
In this quantity, I want to save the total number of unique products. So the product class is like a template for future products with various attributes. Instead of creating new objects manually, I will use this class to create product instances that already have properties like id, name, and amount.
Note, that the static quantity is different from the amount. Amount is the total number of the product while the quantity is the amount of all unique products, not the combination of the amount of the products.
Below is a simplified example for learning purposes.
In the example above, I created a class named products which has attributes like id, name, and amount as well as a static field called quantity. Every time I create a new product, I increase the quantity by one. If I try to access the quantity I can access it through class.
Technically I can also access it through the prototype of the instance as it's still available across instances but the main point is that the values of quantity don't need to change or depend on the instance of the class.
Private static fields
In case you have a static field that has some sensitive information, you can make this static field private as well.
To achieve that, we keep the keyword static but add a hash(#) in the front of the field name.
Let's make our quantity static field private and see what happens. We are not going to change anything but the field privacy.
As you can see, we see some other behaviors. When we check the quantity via product class, it is NaN because we tried to increment quantity while it was not a number. It was not a number because it is undefined. And in the second case, it's undefined as it's not inherited across instances.
How can we make use of it? Let's re-create our class again. To access a static field we are going to use a static method. But to do something with this field, we need to write a public method (regular method). This method will be shared across all instances and we will update quantity with the help of this method via instances. Check this out:
Let's analyze the console log and understand why we have such outputs.
In the first console, we see undefined because the static private field is not available publicly. In the second example, we receive a TypeError because we try to call a method on the class that is not static. Public methods need to be called via class instances. Next, we call the static method directly on the class which gives us the result we need.
The static method belongs to the class so we can call it. Finally, we see undefined again because we try to access property in the class instance which is available only for the class.
Getters and Setters
When it comes to classes, we can create two additional types of class properties called a getter and a setter. The getter starts with the keyword get + the method written in the usual way. The same for the set method, it's written exactly like a regular method but with the set keyword at the start.
The setter always needs to have at least one parameter otherwise it will throw an error "SyntaxError: Setter must have exactly one formal parameter".
As the name suggests, the purpose of get is to get a value while the set method sets the value.
The purpose of setters and getters is to get and set values of encapsulated data inside the class and encapsulated data is usually private. We might expose it later after manipulating it in one way or another.
Even if we expose it, most likely we will hide the way we manipulated it and return not the original value but the result of manipulation.
Let's expand our getter and setter and imagine that we work with a password that needs to be encrypted. The example below will be very minimalistic not the real-life usage for password encryption.
In this example, we created a getter that gets the password field which is private. We also create an encryption private method, to keep the encryption process secret. Inside the encryption, we refer to the password. In the set, we set the password to this newly encrypted value.
Pay attention to the fact that when I pass the value to the setter via the user, I don't pass a value like we usually do. I don't pass it by calling it as a method user.setPass("qwerty123456"). Because even though they do look like methods, they are not the methods we know of.
To be more precise, both getter and setter are property accessors which is why they are called "accessors" not methods.
They are attached to the object as properties which help to control the properties and can read or change them. But when we use the getter or setter properties, behind the scenes there is still a function being called by JavaScript which does all the operations.
These methods bind the properties of an object to a function that will be called whenever we try to get them or set them.
Getters and setters are more in line with object-oriented programming when it comes to accessing properties.
As a result, that is why, when we set a method, we do not call it, we assign a value just like we usually do with properties.
Several things to be aware of. When working with setters, compared to regular methods that set the values, if we misspell the name of the setter, it doesn't throw any errors. Here is the example below:
Did you notice anything unusual and realize why the result didn't update and we got the empty string? It's because when setting the value, we misspelled the name of the set property and wrote setPas instead of setPass.
What would happen if we used a regular method instead? Let's add a method that acts similar to a setter.
In the example above, we created a method that does the same as the setter but as you see, when we misspelled it, we received an error right away.
So does this mean that setters and getters are useless and we shouldn't use them? They're various discussions about it and I didn't find any perfect answers.
We all know how many problems JavaScript has and because of some small mistake like misspelling, we shouldn't stop using getters and setters.
The most modern and simple solution that you can use, is Typescript which would throw an error in such cases and make your life easier. This is how in modern reality many JavaScript issues are solved.
There are some other quick fixes to avoid mistakes like this. On the other hand, there are downsides to these fixes as well.
One of the things we could do is use an object method called freeze. What it does is that it makes an object immutable, so if you misspell a property name and it doesn't exist, it throws an error, as immutability doesn't allow you to add new properties.
As you see, we froze the object and our misspelled property throws an error. Note that we froze the user instance, not the class itself, that's why if we use the proper spelling the setter method will work.
Another similar method is seal, used exactly the same way but compared to freeze it allows changing property values.
Both methods also restrict adding or updating new properties. But what if we need that functionality?
Instead of freezing or sealing the entire object, we can do so only with the property.
Let's change the example above a little and try to freeze the password property instead of the user object.
Before freezing the property right away, we want to allow the encryption first and then freeze it.
As you can see, when we first encrypt the password it works because we check whether the password field is empty. This is the situation when the password is set for the first time.
TheĀ !this.#password in the if statement returns an initial value of a password, an empty string, which is a false value. So we say that if it's false, then encrypt the password and freeze it.
Next, we try to update the password again and it doesn't update because it is not an empty string anymore so the block in the if statement doesn't work anymore.
To summarize, I found mixed opinions about getter and setter accessors and whether they are bad however I have not found enough to prove that they are very bad and you shouldn't be using them. I didn't get to use them much for personal use, that's why I will leave this alone for now.
Inheritance
Inheritance is the attribution of something from someone you are closely connected to.
When it comes to humans, we inherit various genetic attributes from our parents, for example. Or we inherit some things from grandparents and so on. When it comes to classes this term doesn't change its meaning.
With the help of inheritance in classes, we are able to create subclasses of classes that inherit properties, fields, and methods from the superclass (the parent class). Let's create a general employee class.
As you can see, we have an employee class that has properties like firstname, surname, salary, and a method which is an intro of the employee.
Next, we have various departments and they have different requirements, a job, or opportunities. They don't always share everything. They do share the firstname, surname, or salary.
We can create a subclass and extend our superclass of employees. Instead of rewriting the same properties, we can create one more class that includes the exact same properties but also has some other properties. To extend an existing class, a parent class, we can use an additional keyword extend.
In the example above, we created a new class that extends the employee class and adds a new property of stack. The stack will include the programming languages the employee knows and we want only this employee to have this property, that's why we extended it. We copied the employee class but added a stack as we didn't want to add a stack to the employee class.
Let's use our previous knowledge and create a developer class instance. We have a new property stack and we are going to pass a value of that.
But something goes wrong and we receive an error.
When we try to create a new property and use the keyword this in the constructor of the extended class, it warns us that we need to use some keyword super.
The super keyword
The keyword super is used in extended classes (subclasses) in order to initialize the parent constructor as well as inherit any properties from the parent. Let's fix our error above then and use the keyword super which needs to be written inside the constructor method.
There you go, the error is gone and we created a developer class instance. Let's take a look at what it consists of.
And looks at this, the properties like firstname, surname, and salary are also available but they are undefined for some reason.
To use the properties the super keyword isn't enough, we need to retrieve these properties. Let's add a bit more code.
Notice what we added? We added firstname, surname, and salary inside the super as well as the constructor.
Seems like we forgot that we also need to provide some values to these properties. If we want to use them we also need to give them some values. To add the values, we simply need to pass them when creating the class instance.
We added values to our new class instance and now it finally works as intended.
What else did we miss? Correct, we never saw the method intro. The method intro was supposed to be inherited from the superclass aka parent class employee. How to access it?
To access parent class methods, you can once again use the super inside another method that belongs to our subclassā-ādeveloper class.
As a result, we will see the expected intro message with the respective property values. We can also add our new property of stack to this message:
This feature in object-oriented programming is called method overriding, which allows us to implement the same methods in subclasses and override, and replace the existing methods.
However, note that it will not override the method in the parent class. It will belong only to the class instance where we did this override.
This means that if we create a class instance right now, after the developer class instance where we change the intro method, the into method in the employee, the parent class, will remain original.
Inheriting fields
An interesting question, do subclasses inherit fields? Let's find out by creating an address field in a parent class and checking if it can be used in a subclass.
Let me help you understand what I did here.
I added a field address to the parent class called the employee and added a default value of "Unknown". Next, I created a subclass developer where I try to inherit a field via the super in the constructor and then create a new class instance as well as provide a value for the address. Let's check the output.
The value we see for the address did not change. But it did inherit this field because we see the value "Unknown". Well, it's because it's not part of the constructor and it's part of the class so the value remains unchanged. We could change the address field by directly targeting the address field.
But this makes no sense anymore and as if the class lost its purpose. We don't really want to update a single class instance like this and separate the logic from other objects. Now only this object can have such value and we would have to manually do this all the time for new objects.
Evaluation order
Now that we covered some basics, let's understand how classes are created and what the evaluation order exactly is behind the scenes.
- The first thing that is checked during class creation is an extend keyword to understand whether the class creation needs to refer to the parent class or not. In other words, this is done to understand who the main initiator of class creation is.
- Next, it's checked whether there is a constructor present, and if not it's created by default.
- The properties are being checked (variables and functions) that we declare inside the class. If there are any calculations, the values are calculated and saved for later use. The code inside properties is not executed yet.
- Methods (functions) and accessors (getters, setters) are being installed. Class instance methods go to the special list "prototype", and static methods go to the class itself. Private methods are remembered but not installed just yet.
- After this, the class is being initialized, preparing to be used very soon. If there is any extend it's looked at again to check whether it is related to any other class as well as the constructor method where there are kind of instructions on how to build this class. In other words, we are at the point of the class where if we tried to find it by its name, it would throw an error because it's not ready yet.
- Next, properties that needed some calculations are looked at again in the order they were written and the evaluation of these values takes place. The values for fields will be assigned depending on whether it's happening in the parent class or a subclass. In the parent class, it will happen at the start of the constructor function and for subclasses immediately before the super call returns results.
- In the final stage, the class is initialized and ready to create instances.
The instanceof operator
The operator instanceof checks whether the object is an instance of a class or a constructor function. Classes are constructor functions behind the scenes but the constructor functions are not classes.
This operator checks whether the object we are checking was created using the class or constructor function or any ancestors of the prototype chain.
When you create an object with a class, it inherits its properties and methods from its prototype. So instanceof doesn't just check the class but also the prototype chain.
To check whether the object is an instance of a class, you target the object with a name, use the instanceof operator, and then write the class you are checking it against. The return result is either true or false, depending on the result.
Let's use the classes we have created previously and check if the developer object that we have created is an instance of a parent class or its subclass.
In the first console, we see that the developer object is indeed instanceof the developer class as well as the employee class. It's also an instance of an Object. Object is the root of all prototype chains in JavaScript where all the built-in properties are located. On the other hand, the class developer is not an instance of the employee class because classes are not instances.
If you want to find the direct and the exact class the object is an instance of, instead of the instanceof operator you can use the constructor of the object and compare it to the class.
Why do we need instanceof?
When working with more complex data and many classes or objects, sometimes you might want to ensure that you are working with the correct data.
Similar to the example above, we had a property stack in the developer class but didn't have one in the employee. You might try to do something with the property which is not even available.
Also, the same method might change its form a little bit, just like we did in the intro method for the developer class. It can be handy to check if we are working with the correct instance.
Additionally, you can create various conditionals by using the instanceof operator and change responses depending on the instance. On top of everything, it also helps to make the debugging process easier because we can check the original source and look for potential errors there.
Conclusion
In conclusion, we covered some basics and important topics in JavaScript classes and their various features some of which might be a little confusing. We covered the class creation process, the constructor method, properties, fields, and methods that can be static and even private.
Understanding these concepts is crucial in order to work with classes in JavaScript and create well-structured, maintainable code. Classes are something not everyone loves as it is an attempt to switch to object-oriented programming when it's a prototype-based language in the background.
Whether to use it or not and like it or not, it is something up to you but in any case, I hope I provided enough so you can try it out yourself.
Top comments (2)
Awesome stuff! Appreciate ya sharing this one, Ekaterine! š
Thanks!