JavaScript: the definitive guide to classes

Good day, friends!



JavaScript uses the prototypal inheritance model: each object inherits the fields (properties) and methods of the prototype object.



Classes used in Java or Swift as templates or schemas for creating objects do not exist in JavaScript. There are only objects in prototypal inheritance.



Prototypal inheritance can mimic the classic model of class inheritance. To do this, ES6 introduced the class keyword: syntactic sugar for prototypal inheritance.



In this article, we will learn how to work with classes: define classes, their private (private) and public (public) fields and methods, and create instances.



1. Definition: the class keyword



The class keyword is used to define a class:



class User {
    //  
}


This syntax is called a class declaration.



The class may not have a name. Using a class expression, you can assign a class to a variable:



const UserClass = class {
    //  
}


Classes can be exported as modules. Here's an example of the default export:



export default class User {
    //  
}


And here's an example of a named export:



export class User {
    //  
}


Classes are used to create instances. An instance is an object that contains the data and logic of a class.







Instances are created using the new: instance = new Class () operator.



Here's how to create an instance of the User class:



const myUser = new User()


2. Initialization: constructor ()



constructor (param1, param2, ...) is a special method inside a class that initializes an instance. This is where the initial values ​​for the instance fields are set and configured.



In the following example, the constructor sets the initial value for the name field:



class User {
    constructor(name) {
        this.name = name
    }
}


The constructor takes one parameter, name, which is used to set the initial value of the this.name field.



this in the constructor points to the instance being created.



The argument used to instantiate the class becomes a parameter to its constructor:



class User {
    constructor(name) {
        name // 
        this.name = name
    }
}

const user = new User('')


The name parameter inside the constructor has the value 'Pechorin'.



If you don't define your own constructor, a standard constructor is created, which is an empty function that does not affect the instance.



3. Fields



Class fields are variables containing specific information. The fields can be divided into two groups:



  1. Class instance fields
  2. Fields of the class itself (static)


The fields also have two levels of access:



  1. Public (public): fields are available both inside the class and in instances
  2. Private (private): fields are only accessible within the class


3.1. Public fields of class instances



class User {
    constructor(name) {
        this.name = name
    }
}


The expression this.name = name creates an instance field name and assigns an initial value to it.



This field can be accessed using a property accessor:



const user = new User('')
user.name // 


In this case, name is a public field because it is accessible outside of the User class.



When creating fields implicitly inside a constructor, it is difficult to get a list of all fields. For this, the fields must be retrieved from the constructor.



The best way is to explicitly define the fields of the class. It doesn't matter what the constructor does, the instance always has the same set of fields.



The proposal for creating class fields allows you to define fields within a class. In addition, here you can assign initial values ​​to the fields:



class SomeClass {
    field1
    field2 = ' '

    // ...
}


Let's change the code of the User class by defining a public name field in it:



class User {
    name

    constructor(name) {
        this.name = name
    }
}

const user = new User('')
user.name // 


These public fields are very descriptive, a quick glance at the class allows you to understand the structure of its data.



Moreover, a class field can be initialized at the time of definition:



class User {
    name = ''

    constructor() {
        //  
    }
}

const user = new User()
user.name // 


There are no restrictions on access to open fields and their change. You can read and assign values ​​to such fields in the constructor, methods, and outside the class.



3.2. Private fields of class instances



Encapsulation allows you to hide the internal implementation details of a class. Whoever uses the encapsulated class relies on the public interface without going into the details of the class's implementation.



These classes are easier to update when implementation details change.



A good way to hide details is to use private fields. Such fields can only be read and changed within the class to which they belong. Private fields are not available outside the class.



To make a field private, precede its name with a # symbol, for example, #myPrivateField. When referring to such a field, the specified prefix must always be used.



Let's make the name field private:



class User {
    #name

    constructor(name) {
        this.#name = name
    }

    getName() {
        return this.#name
    }
}

const user = new User('')
user.getName() // 
user.#name // SyntaxError


#name is a private field. It can only be accessed inside the User class. The getName () method does this.



However, trying to access #name outside of the User class will throw a syntax error: SyntaxError: Private field '#name' must be declared in an enclosing class.



3.3. Public static fields



In a class, you can define fields that belong to the class itself: static fields. Such fields are used to create constants that store the information the class needs.



To create static fields, use the static keyword before the field name: static myStaticField.



Let's add a new type field to define the type of user: administrator or regular. The static fields TYPE_ADMIN and TYPE_REGULAR are constants for each type of user:



class User {
    static TYPE_ADMIN = 'admin'
    static TYPE_REGULAR = 'regular'

    name
    type

    constructor(name, type) {
        this.name = name
        this.type = type
    }
}

const admin = new User(' ', User.TYPE_ADMIN)
admin.type === User.TYPE_ADMIN // true


To access static fields, use the class name and property name: User.TYPE_ADMIN and User.TYPE_REGULAR.



3.4. Private static fields



Sometimes static fields are also part of the internal implementation of the class. To encapsulate such fields, you can make them private.



To do this, prefix the field name with #: static #myPrivateStaticFiled.



Let's say we want to limit the number of instances of the User class. Private static fields can be created to hide information about the number of instances:



class User {
    static #MAX_INSTANCES = 2
    static #instances = 0
}

name

constructor(name) {
    User.#instances++
    if (User.#instances > User.#MAX_INSTANCES) {
        throw new Error('    User')
    }
    this.name = name
}

new User('')
new User('')
new User('') //     User


The static field User. # MAX_INSTANCES defines the allowed number of instances, and User. # Instances defines the number of instances created.



These private static fields are only available inside the User class. Nothing from the outside world can influence the constraints: this is one of the advantages of encapsulation.



Approx. lane: if you limit the number of instances to one, you get an interesting implementation of the Singleton design pattern.



4. Methods



The fields contain data. The ability to change data is provided by special functions that are part of the class: methods.



JavaScript supports both instance methods and static methods.



4.1. Instance methods



Methods of an instance of a class can modify its data. Instance methods can call other instance methods as well as static methods.



For example, let's define a getName () method that returns the user's name:



class User {
    name = ''

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('')
user.getName() // 


In a class method, as well as in a constructor, this points to the instance being created. Use this to get instance data: this.field, or to call methods: this.method ().



Let's add a new method nameContains (str) that takes one argument and calls another method:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }

    nameContains(str) {
        return this.getName().includes(str)
    }
}

const user = new User('')
user.nameContains('') // true
user.nameContains('') // false


nameContains (str) is a method of the User class that takes one argument. It calls another instance method getName () to get the username.



The method can also be private. To make a method private, use the # prefix.



Let's make the getName () method private:



class User {
    #name

    constructor(name) {
        this.#name = name
    }

    #getName() {
        return this.#name
    }

    nameContains(str) {
        return this.#getName().includes(str)
    }
}

const user = new User('')
user.nameContains('') // true
user.nameContains('') // false

user.#getName // SyntaxError


#getName () is a private method. Inside the nameContains (str) method, we call it like this. # GetName ().



Being private, the #getName () method cannot be called outside of the User class.



4.2. Getters and Setters



Getters and setters are accessors or computed properties. These are methods that mimic fields, but allow you to read and write data.



Getters are used to get data, setters are used to modify it.



To prevent assigning an empty string to the name field, wrap the private #nameValue field in a getter and setter:



class User {
    #nameValue

    constructor(name) {
        this.name = name
    }

    get name() {
        return this.#nameValue
    }

    set name(name) {
        if (name === '') {
            throw new Error('     ')
        }
        this.#nameValue = name
    }
}

const user = new User('')
user.name //  , 
user.name = '' //  

user.name = '' //      


4.3. Static methods



Static methods are functions that belong to the class itself. They define the logic of the class, not its instances.



To create a static method, use the static keyword in front of the method name: static myStaticMethod ().



When working with static methods, there are two simple rules to keep in mind:



  1. A static method has access to static fields
  2. It doesn't have access to instance fields


Let's create a static method to check that a user with the specified name has already been created:



class User {
    static #takenNames = []

    static isNameTaken(name) {
        return User.#takenNames.includes(name)
    }

    name = ''

    constructor(name) {
        this.name = name
        User.#takenNames.push(name)
    }
}

const user = new User('')

User.isNameTaken('') // true
User.isNameTaken('') // false


isNameTaken () is a static method that uses the private static field User. # takenNames to determine which names were used.



Static methods can also be private: static #myPrivateStaticMethod (). Such methods can only be called within the class.



5. Inheritance: extends



Classes in JavaScript support inheritance using the extends keyword.



In the expression, class Child extends Parent {}, the Child class inherits constructors, fields, and methods from Parent.



Let's create a child class ContentWriter that extends the parent class User:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []
}

const writer = new ContentWriter('')

writer.name // 
writer.getName() // 
writer.posts // []


ContentWriter inherits from User the constructor, getName () method, and name field. The ContentWriter itself defines a new posts field.



Note that the private fields and methods of the parent class are not inherited by the child classes.



5.1. Parent constructor: super () in constructor ()


In order to call the constructor of the parent class in the child class, use the special super () function available in the constructor of the child class.



Let the ContentWriter constructor call the parent constructor and initialize the posts field:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }
}

const writer = new ContentWriter('', ['  '])
writer.name // 
writer.posts // ['  ']


super (name) in the child class ContentWriter calls the parent class constructor User.



Note that super () is called in the child constructor before using the this keyword. The super () call "binds" the parent constructor to the instance.



class Child extends Parent {
    constructor(value1, value2) {
        //  !
        this.prop2 = value2
        super(value1)
    }
}


5.2. Parent instance: super in methods


In order to access the parent method inside the child class, use the special super shortcut:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }

    getName() {
        const name = super.getName()
        if (name === '') {
            return ''
        }
        return name
    }
}

const writer = new ContentWriter('', ['  '])
writer.getName() // 


getName () of the child ContentWriter class calls the getName () method of the parent class User.



This is called method overriding.



Note that super can be used for static methods of the parent class as well.



6. Object type check: instanceof



The object instanceof Class expression determines whether an object is an instance of the specified class.



Let's consider an example:



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('')
const obj = {}

user instanceof User // true
obj instanceof User // false


The instanceof operator is polymorphic: it examines the entire class chain.



class User {
    name

    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

class ContentWriter extends User {
    posts = []

    constructor(name, posts) {
        super(name)
        this.posts = posts
    }
}

const writer = new ContentWriter('', ['  '])

writer instanceof ContentWriter // true
writer instanceof User // true


What if we need to define a specific instance class? The constructor property can be used for this:



writer.constructor === ContentWriter // true
writer.constructor === User // false
// 
writer.__proto__ === ContentWriter.prototype // true
writer.__proto__ === User.prototype // false


7. Classes and prototypes



It must be said that class syntax is a nice abstraction over prototypal inheritance. You don't need to reference prototypes to use classes.



However, classes are just a superstructure on prototypal inheritance. Any class is a function that creates an instance when a constructor is called.



The next two examples are identical.



Classes:



class User {
    constructor(name) {
        this.name = name
    }

    getName() {
        return this.name
    }
}

const user = new User('')

user.getName() // 
user instanceof User // true


Prototypes:



function User(name) {
    this.name = name
}

User.prototype.getName = function () {
    return this.name
}

const user = new User('')

user.getName() // 
user instanceof User // true


Therefore, understanding classes requires a good knowledge of prototypal inheritance.



8. Availability of class capabilities



The class capabilities presented in this article are split between the ES6 specification and proposals in the third stage of consideration:







Approx. Per: according to Can I use, support for private class fields is currently 68%.



9. Conclusion



Classes in JavaScript are used to initialize instances using a constructor, and define their fields and methods. The static keyword can be used to define the fields and methods of the class itself.



Inheritance is implemented using the extends keyword. The super keyword allows you to access the parent class from the child.



In order to take advantage of encapsulation, i.e. hide internal implementation details, make fields and methods private. The names of such fields and methods must begin with a # symbol.



Classes are ubiquitous in modern JavaScript.



I hope this article was helpful to you. Thank you for attention.



All Articles