Guide to Understanding Classes in JavaScript

Introduction

When you think about classes and Object Oriented Programming as a paradigm, JavaScript is probably not the first language that comes to mind.

In this guide, we are going to try and push JavaScript further up the list of associations, discussing how to apply Object Oriented Principles while writing JavaScript code. It's worth noting that some of the features we'll be covering are still in development, but most are in production and fully functioning. We'll be updating the guide appropriately as they're being released.

Since JavaScript is mostly used on the web, applying OOP to it can be really useful when, say, getting data from a server (for example, a collection from a MongoDB database) which you can shape up in a class with attributes, as it makes operating with data more intuitive and easier.

What is Object Oriented Programming (OOP)?

Before we get started, let's cover the definition of OOP and some basic principles. If you are already familiar with these concepts, you can go ahead and skip to making a class in JavaScript.

Object Oriented Programing (OOP) is the most popular programming paradigm in Computer Science. It's main building blocks are objects and classes. A class is a blueprint from which objects are created - objects are instances of classes.

You can think of a class as a category, as well. Each class can have attributes/properties which contain different values and describe the states of a class' instance (object). It also contains functions (called methods), which typically change these properties and thus, the state of either the invoking instance or other instances, or return information about it/its neighbors.

Class and Attributes

Say we have a really simple class called ProgrammingLanguage that has two attributes - name and founder, both of which are strings. This is our blueprint for making an object. An object of this class would have attributes and values, say, name = "JavaScript" and founder = "Brendan Eich".

For us to be able to make objects like this from a specific class, that class has to contain a constructor method - or shortly, a constructor. A constructor is practically speaking a manual on how to instantiate an object and assign values. The most common practice for creating a constructor is to name it the same as the class, but it doesn't have to be.

For example, for our ProgrammingLanguage class, we'd define a ProgrammingLanguage() constructor that defines how we assign values to the attributes within the class, when instantiating it. It typically accepts 0..n arguments used as values for the attributes:

class ProgrammingLanguage {
    // Attributes
    String name;
    String founder;
    
    // Constructor method
    ProgrammingLanguage(string passedName, string passedFounder) {
       name = passedName;
       founder = passedFounder;
    }
}

Note: While similar, this isn't JavaScript code and is for illustrative purposes. We'll be using JavaScript when we make a class.

Then, when instantiating this class, we'd pass in some arguments to the constructor, invoking a new object:

ProgrammingLanguage js = new ProgrammingLanguage("JavaScript", "Brendan Eich");

This would create an object of the type ProgrammingLanguage with attributes name="Javascript" and founder="Brendan Eich".

Getter and Setter Methods

There's another set of key methods in OOP - getters and setters. As the name implies, a getter method gets some values, while a setter sets them.

In OOP, they're used to retrieve attributes from an object, rather than accessing them directly, to encapsulate them, perform potential checks, etc. Setters are used to set attributes of objects to the given values - again, in an encapsulated and isolated manner.

Note: To really limit this access, the attributes are typically set to be private (non-accessible outside of the class), when the language in question supports access modifiers.

For instance, you might be prevented if you want to set someone's age to -37 through a setter, which wouldn't be possible to enforce if you were allowed direct access to the attributes.

Setters can be used to either update a value or set it initially, if you use an empty constructor - i.e. a constructor that doesn't set any values initially.

The convention for naming getters and setters is that they should be prefixed with get or set, followed by the attribute they're dealing with:

getName() {
    return name;
}

setName(newName) {
    name = newName;
}

The this Keyword

Classes are self-aware. The this keyword is used to refer to this instance within a class, once it is instantiated. You'll only ever be using the keyword within the class that's referring to itself.

For instance, in the constructor from before, we've used the passed variables passedName and passedFounder, but what if these were just name and founder which make more sense?

Our constructor would look like:

ProgrammingLanguage(String name, String founder) {
    name = name;
    founder = founder;
}

So, which name are we setting to which name? Are we setting the passed value to the attribute or the other way around?

This is where the this keyword kicks in:

ProgrammingLanguage(String name, String name) {
       this.name = name;
       this.founder = founder;
}

Now, it's evident that we're setting the value of this class' attribute to the passed value from the constructor.

The same logic applies to our getters and setters:

getName() {
    return this.name;
}

setName(name) {
   this.name = name;
}

We're getting and setting the name of this class.

The syntax of attributes and constructors as well capitalization conventions vary from language to language, but the main principles of OOP remain the same.

Given how standardized constructors, getters and setters are, most IDEs nowadays have an integrated shortcut for creating a constructor method as well as getters and setters. All you need to do is define the attributes and generate them via the appropriate shortcut in your IDE.

Now that we've gotten more familiar with the OOP concepts, we can dive into OOP in JavaScript.

Making a Class in JavaScript

Note: One difference JavaScript brings about is that when defining classes - you don't have to explicitly state which attributes/fields it has. It's much more pliable and objects of the same class can have different fields if you wish so. Then again, this is discouraged given the fact that it does go against OOP principles, and the standardized practice is partly enforced by having a constructor in which you set all of the attributes (and thus, have some sort of attribute list).

In JavaScript, there is two ways to make a class: using a class declaration and using a class expression.

Using a class declaration, via the class keyword, we can define a class and all of its attributes and methods within the proceeding curly brackets:

class Athlete {}

These can be defined in their respective files or in another file, alongside other code, as a convenience class.

Alternatively, using a class expressions (named or unnamed) lets you define and create them inline:

// Named
let Athlete = class Athlete{}
   
// Unnamed
let Athlete = class {}
   
// Retrieving the name attribute
console.log(Athlete.name);

Retrieving the attribute like this isn't advised, as in true OOP spirit - we shouldn't be able to access a class' attributes directly.

Since we don't have a constructor, nor getters and setters, let's go ahead and define those.

Creating a Constructor, Getters and Setters in JavaScript

Another thing to note is that JavaScript enforces the constructor's name. It has to be named constructor(). This is also the place where you essentially define your class' attributes, albeit a bit more implicitly than in languages such as Java:

class Athlete{
    constructor(name, height, weight) {
        this._name = name;
        this._height = height;
        this._weight = weight;
    }
}

const athlete = new Athlete("Michael Jordan", 198, 98);

If you'd like to define the attributes beforehand, you can but it's redundant given JavaScript's nature, unless you're trying to create private properties. In any case, you should prefix your attribute names with _.

Since JavaScript didn't use to support encapsulation out of the box, this was a way to tell users of your class not to access the attributes directly. If you ever see an underscore before an attribute's name - do yourself and the creator of the class a favor and don't access it directly.

Note: It was technically possible to produce private attributes within JavaScript classes, but it was not widely adopted or used - Douglas Crockford proposed hiding the variables within closures to achieve this effect.

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

You can further annotate your intent via the @access annotation, signifying what access level you'd want the attribute to have:

class Athlete {
    /** @access private */
   _name;
    
    constructor(name) {
        this._name = name;
    }
    
    getName() {
        return this._name;
    }
    
    setName(name) {
        this._name = name;
    }
}

You can then instantiate an object, as well as get and set its attribute:

let athlete = new Athlete('Michael Jordan');
console.log(athlete.getName());

athlete.setName('Kobe Bryant');
console.log(athlete.getName());

This results in:

Michael Jordan
Kobe Bryant

You can also access the property directly, though:

console.log(athlete._name); // Michael Jordan

Setting Fields as Private

Finally, private fields were introduced, and are prefixed with #. They actually enforce the usage of the fields to be private and they cannot be accessed outside of the class - only through methods that expose it:

class Athlete {
    /** @access private */
    #name;
    
    constructor(name) {
        this.#name = name;
    }
    
    getName() {
        return this.#name;
    }
    
    setName(name) {
        this.#name = name;
    }
}

let athlete = new Athlete('Michael Jordan');
console.log(athlete.getName()); // Michael Jordan
console.log(athlete.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class

This way, encapsulation is actually achieved, as users can only ever access attributes through vetted methods that can validate the returned values, or stop them from setting unexpected values, such as assigning a number instead of a string to the #name attribute.

Note: To mark an attribute as private, you have to declare it before the getters and setters. This feature has been live since 2018 (Babel 7.0+) but it might not work on some older environments.

The get and set Keywords

Alternatively, JavaScript has a special set of keywords - get and set, which can be used to make getters and setters. When used, they bind certain attributes to the functions invoked when you'd like to access them.

It's convention to use the same name between an attribute and the getter/setter methods bound by get and set, without a prefix (it'd be redundant):

class Athlete {

    constructor(name) {
        this._name = name;
    }
    
    get name() {
        return this._name;
    }
    
    set name(name) {
        this._name = name;
    }
}

let athlete = new Athlete("Michael Jordan");

console.log(athlete.name); // Output: Michael Jordan

athlete.name = "Kobe Bryant";
console.log(athlete.name); // Output: Kobe Bryant

Although it may look like it, we're not accessing the _name attribute directly. We're implicitly calling the name() method, by trying to access the attribute, when that request is rerouted to the get name() method. To make this clearer, let's modify the get name() method's body:

get name() {
    return "Name: " + this._name;
}

Now, this:

let athlete = new Athlete('Michael Jordan')
console.log(athlete.name);

Results in:

Name: Michael Jordan

Note: Another reason to add an underscore (_) to attribute names is if you'll be using this approach for defining getters and setters. If we were to just use name as the attribute, it'd be ambiguous, given the fact that name can also refer to get name().

This would kick off a recursive loop as soon as we try to instantiate the class, filling up the call stack until it runs out of memory:

class Athlete {
    constructor(name) {
        this.name = name;
    }
  
    get name() {
        return this.name;
    }
    
    set name(name) {
        this.name = name;
    }
}

let athlete = new Athlete('Michael Jordan');
console.log(athlete.name);

Which results in:

script.js:12
        this.name = name;
                  ^

RangeError: Maximum call stack size exceeded

Using Getter/Setter Functions or Keywords?

Which approach is better?

The community is divided in the choice between these, and some developers prefer one over the other. There's no clear winner and both approaches support OOP principles by allowing encapsulation and can return and set private attributes.

Defining Class Methods

We've already defined some methods before, namely, the getter and setter methods. In much the same way, we can define other methods that perform other tasks.

There are two main ways to define methods - in-class and out-of-class.

So far, we've been utilizing in-class definitions:

class Athlete {
 // Constructor, getters, setters
 
    sayHello() {
        return "Hello, my name is " + this.name;
    }
}
console.log(athlete.sayHello()) // Hello, my name is Kobe Bryant

Alternatively, you can explicitly create a function via a function declaration, outside of a class:

class Athlete {
    // Class code
}

athlete.sayHello = function() {
    return "Hello, my name is " + athlete.name;
}

let athlete = new Athlete("Kobe Bryant");
console.log(athlete.sayHello()) // Output: Hello, my name is Kobe Bryant

To JavaScript, either of these approaches are the same, so you can choose whichever one suits you better.

Class Inheritance in JavaScript

A key concept of OOP is class inheritance. A subclass (child class) can be extended from a class and define new properties and methods, while inheriting some from its superclass (parent class).

An Athlete can be a BasketballPlayer, TennisPlayer or a FootballPlayer but all three of these are an instance of an Athlete.

In JavaScript, the extends keyword is used to create a subclass:

// Athlete class definition

class BasketballPlayer extends Athlete {
    constructor(name, height, weight, sport, teamName) {
        super(name, height, weight);
        this._sport = sport;
        this._teamName = teamName;
    }
    
    get sport() {
        return this._sport;
    }
    
    get teamName() {
        return this._teamName;
    }
}

const bp = new BasketballPlayer("LeBron James", 208, 108, "Basketball", "Los Angeles Lakers");

We've created an object of the BasketballPlayer class which contains the attributes used in the Athlete class, as well as two new attributes, sport and teamName - which are specific for the BasketballPlayer class.

Similar to how this refers to this class, super() refers to the superclass. By calling super() with arguments, we're calling the constructor of the superclass, setting a few attributes, before setting the new ones specific to the BasketballPlayer class.

When we use the extends keyword, we inherit all the methods and attributes that are present in the superclass - that means we inherited the sayHello() method, getters and setters and all attributes. We can create a new method using that one and adding more to it, like this:

class BasketballPlayer extends Athlete {
    // ... previous code
    
    fullIntroduction() {
        return this.sayHello() + " and I play " + this.sport + " in " + this.teamName;
    }
}

const bp = new BasketballPlayer("LeBron James", 208, 108, "Basketball", "Los Angeles Lakers");
console.log(bp.fullIntroduction());

Which will result in:

Hello, my name is LeBron James and I play Basketball in Los Angeles Lakers

Note: We haven't defined a sayHello() method in the BasketballPlayer class, but can still access it via this. How so? Isn't it part of the Athlete class? It is. But BasketballPlayer inherited this method so it's as good as defined in the BasketballPlayer class.

The instanceof Operator

The instanceof operator is used to check whether some object is an instance of a certain class. The return type is a boolean:

let bp = new BasketballPlayer();
let athlete = new Athlete();

console.log(bp instanceof BasketballPlayer); // Output: true
console.log(bp instanceof Athlete); // Output: true

console.log(athlete instanceof Athlete); // Output: true
console.log(athlete instanceof BasketballPlayer); // Output: false

A BasketballPlayer is an Athlete so bp is an instance of both. On the other hand, an Athlete doesn't have to be a BasketballPlayer, so athlete is only an instance of Athlete. If we instantiate the Athlete as a basketball player, such as bp, they are an instance of both.

Conclusion

In this guide, we've taken a look at some of the basic principles of OOP as well as how classes work in JavaScript. JavaScript isn't fully suited for OOP quite yet, but strides are being made to adapt the functionality further.

We've explored class definitions, attributes, getters, setters, encapsulation, class methods and inheritance.

Last Updated: October 14th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms