Before ES6, JavaScript does not really support classes and classical inheritance as the primary way of defining similar and related objects when it was created. This was very confusing back then, especially if you are coming from a C#, Java, C++ background. Moreover, the closest equivalent to a class before ES6 was creating a constructor-function and then assigning methods to the constructor’s prototype, an approach which typically called for creating a custom type.
However, today due to ES6, developers can now create their classes. We are going to tackle that in this article. Lastly, I still believe in more code samples to eventually learn the language even better, hence, we are going to focus more on the syntax to familiarize ourselves in this OOP core concepts.
We are going to use Node.js as our platform to explore the concepts of OOP using JavaScript/ES6. Therefore, Node.js should be installed within your operating system.
In ES6, class declaration looks pretty similar to classes in other languages. Class declarations begin with the class keyword followed by the name of the class. Let's take a look at the example below.
To create a class in JavaScript/ES6 you just need the keyword
class then followed by its name. In our case, the name of the class is "Customer" without the quotes.
Class constructor
Yes, I know the previous example was very easy to understand. Well, that's how we learn we start with simple examples first as we progress with the hard topics.
Now, let's move on with constructors.
Basically, constructors are executed once we have created a new instance of a class. Another thing to note, JavaScript doesn't support constructor overloading. Therefore you can't have more than one constructor, within your class, or else the JavaScript compiler will throw an error.
Let us an example of a constructor.
- class Customer {
-
- constructor() {
-
- console.log("\n");
- console.log("------------start inside the constructor-----------");
- console.log("called automatically when a new instance is created");
- console.log("-------------end inside the constructor------------");
- }
- }
Let's try to test again the Customer class if the constructor is really executed when we have created a new instance of the Customer class.
- import { Customer} from '../constructors/learning-constructors';
- import { expect } from 'chai';
-
- describe("Test if constructors are called when a new instance is created",
- () => {
- it('returns instanceof customer', () => {
-
- let customer = new Customer();
-
- expect(customer).to.be.an('object');
- expect(customer).to.be.an.instanceOf(Customer);
-
- });
- });
Output
Wasn't that great? We have seen how constructors work. Now, you might be thinking that there could be a hack or another way to have a constructor overloading.
Well, in my experience you can use object destructuring assignment as parameters for the constructor. That's the closest thing to the constructor overloading, in my opinion. Don't worry we are going to use that in the class properties section. If you want to learn more about it, please this my post at C# corner about
JavaScript Destructing Assignment.
Moreover; there are many ways to do this is just one of them. Here are some ways of doing it: passing an object, using default parameters, and object destructuring assignment.
Class getters and setters
Now, we have seen how to declare a class and understand the constructor.
In this section, we are going to see how to create these getters and setters just like those getters and setters in Java/C#/C++.
- class Customer{
-
- #_firstName;
- #_lastName;
- #_isPrimeMember;
-
- constructor(firstName, lastName, isPrimeMember){
- this.#_firstName = firstName;
- this.#_lastName = lastName;
- this.#_isPrimeMember = isPrimeMember;
- }
-
- get firstName(){
- return this.#_firstName;
- }
-
- set firstName(value){
- this.#_firstName = value;
- }
-
- get lastName(){
- return this.#_lastName;
- }
-
- set lastName(value){
- this.#_lastName = value;
- }
-
- get isPrimeMember(){
-
- return this.#_isPrimeMember;
- }
- }
Probably, you are asking yourself what's with the
pound sign (#). It is basically saying to the JS compiler that we have a private member within our class.
There are many ways to mimic private members within the JavaScript class, and this is one way.
Now, in order to fully demonstrate getters and setters, we really need some kind of private or backing fields so we could appreciate the getters and setters.
One thing to point out: this feature is not yet totally supported, that's why we are dependent on the following packages babel/plugin-proposal-class-properties and babel/plugin-proposal-private-methods, which makes this feature possible.
Let's see how we can test this class.
- import { Customer } from '../getters-setters/getter-setters';
- import { expect } from 'chai';
-
- describe("Test the getters and setters of the Customer class",
- () => {
-
- let firstName = "Jin", lastName = "Necesario", isPrimeMember = true;
-
- let customer = new Customer(firstName, lastName, isPrimeMember);
-
- it('check wheter we have a valid instance', () => {
-
- expect(customer).to.be.an('object', 'customer is an object');
- expect(customer).to.be.an.instanceOf(Customer, 'customer is an instance of the Customer class');
- });
-
- it('Checks the object if has valid getters', () => {
-
-
- expect(customer.firstName).to.be.a('string').and.equal(firstName, `Customer's firstname is ${firstName}`);
- expect(customer.lastName).to.be.a('string').and.equal(lastName, `Customer's firstname is ${lastName}`);
- expect(customer.isPrimeMember).to.be.a('boolean').and.equal(isPrimeMember, `Customer's firstname is ${isPrimeMember}`);
-
- });
-
- it('Checks whether the object if has valid setters', () => {
-
-
- customer = { firstName: "Scott", lastName: "Summers", isPrimeMember: false};
-
- expect(customer.firstName).to.be.a('string').and.equal('Scott', `Customer's firstname is ${firstName}`);
- expect(customer.lastName).to.be.a('string').and.equal('Summers', `Customer's firstname is ${lastName}`);
- expect(customer.isPrimeMember).to.be.a('boolean').and.equal(false, `Customer's firstname is ${isPrimeMember}`);
-
- });
- });
Output
Class Properties and Static Properties
Of course, it won't be complete without dealing with properties. In this section, we are going to see how to access those properties of a class.
Like in other programming languages, this keyword is a reference to the current object - the object whose constructor is being called. However, the properties in JavaScript are by default public because you can access them directly. Don't forget to use the keyword static to identify a property as a static one.
One thing to point out, we have used the object destructuring assignment as the usage of the constructor.
Let's see this in action.
- class Customer {
-
- static MinAgeForEntry = 20;
-
- constructor({firstName, lastName, birthDate, country}){
- this.firstName = firstName;
- this.lastName = lastName;
- this.birthDate = birthDate;
- this.country = country;
- }
- }
Let's create a test for this class that has a static property and different instance properties.
- import { Customer } from '../class-with-methods/class-with-properties';
- import { expect } from 'chai';
-
- describe("Test if class have the following properties firstName,lastName,birthDate and country",
- () => {
-
- let customer = new Customer({ firstName: "Jin", lastName: "Necesario", birthDate: "1/1/2000", country: "PHP" });
-
- it('check if the customer has a static property and check its value', () => {
-
- expect(Customer).itself.to.have.property("MinAgeForEntry")
- .to.be.equal(20, '20 should be the value');
- });
-
- it('check if the customer object have the following properties firstName, lastName,birthDate and country', () => {
-
- expect(customer).to.have.property("firstName");
- expect(customer).to.have.property("lastName");
- expect(customer).to.have.property("birthDate");
- expect(customer).to.have.property("country");
-
- });
-
- it('check the properties values', () => {
-
- expect(customer.firstName).to.be.equal("Jin");
- expect(customer.lastName).to.be.equal("Necesario");
- expect(customer.birthDate).to.be.equal("1/1/2000");
- expect(customer.country).to.be.equal("PHP");
-
- });
- });
As you can see from the tests, once we have passed an argument with values within the constructor and set the properties with those values you can basically access with ease. However, if you want some kind of protection you can go back to the previous example which shows the private fields of a class.
Output
Class Methods and Static Methods
You probably are already familiar with functions, functions are also called methods, just remember that they are interchangeable.
When defining methods there is no special keyword just define its name and construct its body. However, when defining a static method you need to put the static keyword first then its name.
Let's see an example below.
- class Customer {
-
- constructor({firstName, lastName, birthDate, country}){
- this.firstName = firstName;
- this.lastName = lastName;
- this.birthDate = birthDate;
- this.country = country;
- }
-
- static isLeapYear(){
-
- let year = new Date().getFullYear();
-
- return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
- }
-
- getFullName(){
- return `Mr./Ms. ${this.lastName}, ${this.firstName}`
- }
-
- getCustomerCompleteInfo(){
- return `Mr./Ms. ${this.lastName}, ${this.firstName}`
- + ` was born in ${this.country} on ${this.birthDate}`
- + ` and currently ${new Date().getFullYear() - new Date(this.birthDate).getFullYear()} years of age.`
- }
- }
Let's create a test for this class that will check the values of both static and instance methods.
- import { Customer } from '../class-with-methods/class-with-methods';
- import { expect } from 'chai';
-
- describe("Test if class have the following methods getFullName, getCustomerCompleteInfo & isLeapYear",
- () => {
-
- let customer = new Customer({firstName : "Jin", lastName : "Necesario", birthDate : "1/1/2000", country :"PHP"});
-
- it('checks the methods', () => {
-
- let completeInfo = 'Mr./Ms. Necesario, Jin was born in PHP on 1/1/2000 and currently 20 years of age.';
-
-
- expect(customer.getFullName()).to.be.equal("Mr./Ms. Necesario, Jin");
- expect(customer.getCustomerCompleteInfo()).to.be.equal(completeInfo);
-
-
- expect(Customer.isLeapYear()).to.be.a('boolean').to.be.oneOf([true,false]);
- });
- });
Output
Inheriting constructors & properties
In this section, we are going to see how a child class can extend a parent class. It can be done using the extends keyword. Hence, by using the keyword extends, we are basically inheriting the parent class.
Don't forget to always call the parent's constructor, even if it is empty when you are inside the child class, or else an error will be thrown.
- class Person {
- constructor(firstName, lastName, age){
- this.firstName = firstName;
- this.lastName = lastName;
- this.age = age;
- }
- }
-
- class Customer extends Person{
-
- constructor(firstName, lastName, age, isPrimeMember){
-
- super(firstName, lastName, age);
-
- this.isPrimeMember = isPrimeMember;
- }
- }
-
- export { Person, Customer}
Let's try to test if we have correctly inherited the parent class.
- import { Person, Customer } from '../inheritance/inheritance-constructor';
- import { expect } from 'chai';
-
- describe("Test parent constructor",
- () => {
- let customer = new Customer("Jin", "Necesario", 120, true);
-
- it('Test if have inherited the properties that was initialized via super', () => {
-
-
- expect(customer).to.be.an('object');
- expect(customer).to.be.instanceOf(Person);
-
-
- expect(customer).to.have.property("firstName");
- expect(customer).to.have.property("lastName");
- expect(customer).to.have.property("age");
- });
- });
Output