Skip to main content

Classes

Object literal syntax makes it possible to create a single object. However, often you need to create many objects of the same type with the same set of properties, but different values and methods for interacting with them. All this must be done dynamically, during program execution. To do this, use classes, a special syntax for declaring functions to create objects.

Class declaration

A class declaration begins with the keyword class, followed by the class name and curly braces, i.e. its body. Classes are usually named with a capital letter, and the name should describe the type of object being created (noun).

class User {
// Class body
}

const mango = new User();
console.log(mango); // {}

const poly = new User();
console.log(poly); // {}

The result of calling new User() is an object called class instance because it contains the data and behavior described by the class.

Note

How you build a class depends on what you need. In our case, the class is a user, so add fields for the name and email.

Class constructor

To initialize an instance, the class has a constructor method. If not declared, a default constructor is created, an empty function that does not change the instance.

class User {
// Syntax for declaring a class method
constructor(name, email) {
// Initializing instance properties
this.name = name;
this.email = email;
}
}

const mango = new User("Mango", "mango@mail.com");
console.log(mango); // { name: 'Mango', email: 'mango@mail.com' }

const poly = new User("Poly", "poly@mail.com");
console.log(poly); // { name: 'Poly', email: 'poly@mail.com' }

Calling a class with the new operator creates a new object and calls the constructor in the context of that object. That is, this inside the constructor will refer to the newly created object. This enables you to add properties to each object that have the same name but different values.

The name and email properties are called public properties because they will be own properties of the instance object and can be accessed using dot notation.

Parameter object

The class can deal with a large amount of input data for the properties of the future object. Therefore, you can also apply the “Parameter Object” pattern to them, passing a single object with properties having logical names instead of an unrelated set of arguments.

class User {
// Object destructuring
constructor({ name, email }) {
this.name = name;
this.email = email;
}
}

const mango = new User({
name: "Mango",
email: "mango@mail.com",
});
console.log(mango); // { name: "Mango", email: "mango@mail.com" }

const poly = new User({
name: "Poly",
email: "poly@mail.com",
});
console.log(poly); // { name: "Poly", email: "poly@mail.com" }

Class methods

To work with the properties of the future instance, class methods should be used: functions that will be available to the instance in its prototype.

class User {
constructor({ name, email }) {
this.name = name;
this.email = email;
}

// getEmail method
getEmail() {
return this.email;
}

// changeEmail method
changeEmail(newEmail) {
this.email = newEmail;
}
}

Private properties

Encapsulation is a concept to hide the internal details of a class. A class user should only have access to the public interface, i.e. to a set of public properties and methods of the class.

In classes, encapsulation relies on private properties, which can only be accessed from within the class.

Let's say the user's email should be inaccessible for modification from outside, that is, should be private. By adding # to the property name, you make it private. Declaring a private property before initializing in the constructor is a mandatory step.

class User {
// Optional declaration of public properties
name;
// Mandatory declaration of private properties
#email;

constructor({ name, email }) {
this.name = name;
this.#email = email;
}

getEmail() {
return this.#email;
}

changeEmail(newEmail) {
this.#email = newEmail;
}
}

const mango = new User({
name: "Mango",
email: "mango@mail.com",
});
mango.changeEmail("mango@supermail.com");
console.log(mango.getEmail()); // mango@supermail.com
console.log(mango.#email); // Error, this property is private

Class methods can also be made private, that is, available only in the class body. To do this, precede their names with #.

Getters and setters

Getters and setters are a shorter syntax for declaring methods to interact with properties. Getters and setters simulate regular public properties of classes, but make it possible to change other properties in a more convenient way. A getter is executed when trying to get a property value, and a setter is executed when trying to change it.

Getters and setters are good for simple read and modify operations on properties, especially private ones like their public interface. They are not suitable for dealing with a property that stores an array or object.

class User {
#email;

constructor({ name, email }) {
this.name = name;
this.#email = email;
}

// email getter
get email() {
return this.#email;
}

// email setter
set email(newEmail) {
this.#email = newEmail;
}
}

You have declared an email getter and setter with the keywords get and set before the property name. Within these methods, you either return the value of the # email private property, or change its value. Getters and setters are paired and must be named the same.

const mango = new User({ name: "Mango", email: "mango@mail.com" });
console.log(mango.email); // mango@mail.com
mango.email = "mango@supermail.com";
console.log(mango.email); // mango@supermail.com

When accessing mango.email, you call the get email() {...} getter and execute its code. When trying to write mango.email = "mango@supermail.com", you call the set email(newEmail) {...} setter, and the string mango@supermail.com will be the value of the newEmail parameter.

The advantage is that these are methods, which means that when writing, you can execute additional code, for example, checking something, in contrast to doing the same directly to the property.

set email(newEmail) {
if(newEmail === "") {
console.error("Error! Email cannot be an empty string!");
return;
}

this.#email = newEmail;
}

Static properties

In addition to the public and private properties of the future instance, in the class you can declare its own properties that are available only to the class, but not to its instances – static properties (static). They are useful for storing information related to the class itself.

Add a private property, role, to the user class – its role that defines a set of rights, for example, administrator, editor, user, etc. Then store possible user roles as a static property, Roles, an object with properties.

Static properties are declared in the class body. The keyword static must precede the property name.

class User {
// Declaring and initializing a static property
static Roles = {
ADMIN: "admin",
EDITOR: "editor",
};

#email;
#role;

constructor({ email, role }) {
this.#email = email;
this.#role = role;
}

get role() {
return this.#role;
}

set role(newRole) {
this.#role = newRole;
}
}

const mango = new User({
email: "mango@mail.com",
role: User.Roles.ADMIN,
});

console.log(mango.Roles); // undefined
console.log(User.Roles); // { ADMIN: "admin", EDITOR: "editor" }

console.log(mango.role); // "admin"
mango.role = User.Roles.EDITOR;
console.log(mango.role); // "editor"

Static properties can also be private, that is, available only within the class. For this, the property name must begin with #, just like private properties. Accessing a private static property outside of the class body will throw an error.

Static methods

In a class, you can declare not only the methods of the future instance, but also methods available only to the class, i.e. static methods that can be both public and private. The declaration syntax is similar to static properties, except that the value is a method.

class User {
static #takenEmails = [];

static isEmailTaken(email) {
return User.#takenEmails.includes(email);
}

#email;

constructor({ email }) {
this.#email = email;
User.#takenEmails.push(email);
}
}

const mango = new User({ email: "mango@mail.com" });

console.log(User.isEmailTaken("poly@mail.com"));
console.log(User.isEmailTaken("mango@mail.com"));

Static methods have the following feature: when they are called, the keyword this refers to the class itself. This means that a static method can access static properties of a class, but not the properties of an instance. It makes sense, because static methods are called by the class itself, not its instances.

Class inheritance

The keyword extends enables class inheritance, when one class (child, derived) inherits the properties and methods of another class (parent).

class Child extends Parent {
// ...
}

In the expression class Child extends Parent, the Child child class inherits (extends) from the Parent parent class.

This means that you can declare a base class that stores common characteristics and methods for a group of derived classes, which inherit the properties and methods of the parent and, besides, add their own unique ones.

For example, the application has users of different roles – administrator, article writer, content manager, etc. Each type of user has a set of common characteristics, such as mail and password, but also some unique ones.

Having made independent classes for each user type, you duplicate common properties and methods, and when you need to change, for example, the name of a property, you will have to do it for all classes, which is inconvenient and time consuming.

Instead, you can make a general class, User, which will store a set of common properties and methods, and then make classes for each user type that inherit this set from the User class. When something general needs to be changed, it will be enough to change only the code of the User class.

class User {
#email;

constructor(email) {
this.#email = email;
}

get email() {
return this.#email;
}

set email(newEmail) {
this.#email = newEmail;
}
}

class ContentEditor extends User {
// ContentEditor class body
}

const editor = new ContentEditor("mango@mail.com");
console.log(editor); // { email: "mango@mail.com" }
console.log(editor.email); // "mango@mail.com"

The ContentEditor class inherits from the User class its constructor, email getter and setter, and the public property of the same name. It is important to remember that private properties and methods of the parent class are not inherited by the child class.

Child class constructor

First of all, in the child class constructor, you need to call a special function, super(arguments) – this is an alias for the parent class constructor. Otherwise, when trying to access this in the child class constructor, an error will be generated. When calling the parent class constructor, pass the arguments necessary to initialize the properties.

class User {
#email;

constructor(email) {
this.#email = email;
}

get email() {
return this.#email;
}

set email(newEmail) {
this.#email = newEmail;
}
}

class ContentEditor extends User {
constructor({ email, posts }) {
// Calling the constructor of the User parent class
super(email);
this.posts = posts;
}
}

const editor = new ContentEditor({ email: "mango@mail.com", posts: [] });
console.log(editor); // { email: 'mango@mail.com', posts: [] }
console.log(editor.email); // 'mango@mail.com'

Child class methods

In a child class, you can declare methods that will only be available to its instances.

// Imagine there is a declaration of the User class above

class ContentEditor extends User {
constructor({ email, posts }) {
super(email);
this.posts = posts;
}

addPost(post) {
this.posts.push(post);
}
}

const editor = new ContentEditor({ email: "mango@mail.com", posts: [] });
console.log(editor); // { email: 'mango@mail.com', posts: [] }
console.log(editor.email); // 'mango@mail.com'
editor.addPost("post-1");
console.log(editor.posts); // ['post-1']