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