Classes in JavaScript Explained – What Is a JavaScript Class?
A JavaScript class is an object constructor that the new
keyword uses to create a new object instance.
Example of a JavaScript Class
// Define a JavaScript class:class Name {}
// Create an object instance from the Name class:const yourName = new Name();
// Check yourName's content:yourName;
// The invocation above will return an empty object: { }
The snippet above used the new
keyword to create a new object instance from the class constructor.
Why Classes in JavaScript?
Classes provide a way to create a template for creating objects that have access to private data through public methods.
In other words, classes help you encapsulate your data while providing users indirect access to an instance’s internal workings. This helps you provide users with a clean and friendly interface that is independent of an object’s internal implementations.
For instance, Date
is a JavaScript class that allows you to access its date data through its public methods, such as getDate()
, setDate()
, and getFullYear()
.
Syntax of a JavaScript Class
class NameOfClass { // class's body}
A class is composed of four components:
- A
class
keyword - The class’s name
- A code block (
{...}
) - The class’s body
Types of JavaScript Classes
The three types of JavaScript classes are:
- Class declaration
- Class expression
- Derived class
Let’s discuss each type.
What is a JavaScript class declaration?
A class declaration is a class created without assigning it to a variable.
Here’s an example:
class Numbers {}
The class above is a class declaration because we defined it without storing it in a variable.
What is a JavaScript class expression?
A class expression is a class you create and assign to a variable.
Here’s an example:
const myClassExpr = class Numbers {};
The class above is a named class expression that we assigned to the myClassExpr
variable.
You can also write the snippet above as an anonymous class expression like so:
const myClassExpr = class {};
The class above is an anonymous function expression assigned to the myClassExpr
variable.
Let’s now discuss derived classes.
What is a derived class in JavaScript?
A derived class is a class that extends the public and static features of an existing class.
In other words, a derived class is the child of a parent class.
Syntax of a derived class
We use the extends
keyword to create a derived class.
Here’s the syntax:
class DerivedClass extends BaseClass { // derived class's body}
Once you extend a child class to a parent class, the derived class will inherit all its base class’s class fields.
Example: How to use a base class’s features in a derived class
// Create a new class:class Name { myName = "Oluwatobi";}
// Create a derived class:class Bio extends Name {}
// Create a new object instance:const myBio = new Bio();
// Check myBio's current value:myBio;
// The invocation above will return: { myName: "Oluwatobi" }
The Bio
class inherited its parent’s property because we used the extends
keyword to assign the Name
class as the derived class’s dunder proto.
Note: A derived class’s class field will override its parent class’s property with the same name. For example, consider the following code:
// Create a new class:class Name { myName = "Oluwatobi";}
// Create a derived class:class Bio extends Name { myName = "Sofela";}
// Create a new object instance:const myBio = new Bio();
// Check myBio's current value:myBio;
// The invocation above will return: { myName: "Sofela" }
Now that we know the syntax and types of JavaScript classes let’s look at the main components in one piece.
Components of a JavaScript Class
The main features of a JavaScript class are as follows:
- A
class
keyword - The class’s name
- The
extends
clause - A code block (
{...}
) - The class’s body
- A
constructor
method super()
function callersuper
property accessor- Instance class fields
- Prototypal class fields
- Private class fields
- Static class fields
- Static initialization blocks
Let’s look at these features in a class declaration.
class ChildClass extends ParentClass { constructor(parameter) { super(parameter); } instanceClassField = "Value can be any valid JavaScript data type"; prototypalClassField() { // prototypalClassField's body } #privateClassField = "Value can be any valid JavaScript data type"; static classField = "Value can be any valid JavaScript data type"; static classFieldWithSuperValue = super.parentProperty; static #privateClassField = "Value can be any valid JavaScript data type"; static { // Static initialization block's body }}
The constructor function equivalence of the snippet above looks like this:
function ChildClass() { this.instanceClassField = "Value can be any valid JavaScript data type";}
Object.setPrototypeOf(ChildClass, ParentClass);
ChildClass.prototype.prototypalClassField = function () { // prototypalClassField's body};
ChildClass.staticClassField = "Value can be any valid JavaScript data type";
ChildClass.staticClassFieldWithSuperValue = Object.getPrototypeOf(ChildClass).parentProperty;
(function () { // Static initialization block's body})();
How Does a JavaScript Class Help with Encapsulation?
Classes let you prevent external code from interacting with internal class fields. Instead, external code would use public methods to operate on the class’s internal implementations.
For instance, consider the following code:
// Create a new class:class Name { // Create a private class field data: #myName = "Oluwatobi";
// Create a publicly available method: showMyName() { return this.#myName; }
// Create another publicly available method: updateMyName(value) { this.#myName = value; }}
// Create a new object instance:const bio = new Name();
// Check the instance's data value:bio.myName;
// The invocation above will return: undefined
The snippet above encapsulated Name
’s data because it defined myName
as a private feature and provided two public methods for users to read and update the class’s internal implementation.
Consequently, the bio
instance object knows nothing about the class’s internal data and cannot interact with it directly.
Whenever users need to access the encapsulated data, they would use the publicly available methods like so:
// Check the instance's data value:bio.showMyName();
// The invocation above will return: "Oluwatobi"
// Update the instance's data value:bio.updateMyName("Sofela");
// Check the instance's data value:bio.showMyName();
// The invocation above will return: "Sofela"
Encapsulating your data is an excellent way to keep your class clean. It prevents minor internal refactoring from breaking users’ code.
For instance, consider the following code:
// Create a new class:class Name { // Create a public class field data: myName = "Oluwatobi";}
// Create a new object instance:const bio = new Name();
// Check the instance's data value:bio.myName;
// The invocation above will return: "Oluwatobi"
// Update the instance's data value:bio.myName = "Sofela";
// Check the instance's data value:bio.myName;
// The invocation above will return: "Sofela"
Since the snippet above did not encapsulate the class’s data, refactoring the class field’s name would break users’ code.
Here’s an example:
class Name { // Update the data's name from myName to myFirstName: myFirstName = "Oluwatobi";}
// Create a new object instance:const bio = new Name();
// Check the instance's data value:bio.myName;
// The invocation above will return: undefined
The snippet above returned undefined
because refactoring the class’s internal implementation broke the user’s bio.myName
code. For the application to work appropriately, the user must update every instance of the code (which can be burdensome for large projects).
However, encapsulation prevents such refactoring from breaking the user’s code.
Here’s an example:
class Name { // Update the data's name from myName to myFirstName: #myFirstName = "Oluwatobi";
// Create a publicly available method: showMyName() { return this.#myFirstName; }
// Create another publicly available method: updateMyName(value) { this.#myFirstName = value; }}
// Create a new object instance:const bio = new Name();
// Check the instance's data value:bio.showMyName();
// The invocation above will return: "Oluwatobi"
// Update the instance's data value:bio.updateMyName("Sofela");
// Check the instance's data value:bio.showMyName();
// The invocation above will return: "Sofela"
You can see that refactoring the class’s internal implementation did not break the user’s code. That’s the beauty of encapsulation!
Encapsulation allows you to provide users with an interface independent of the class’s underlying data. Therefore, you minimize the likelihood of users’ code breaking when you alter internal implementations.
Important Stuff to Know about JavaScript Classes
Here are five essential facts to remember when using JavaScript classes.
1. Declare your class before you access it
Classes are like constructor functions but have the same temporal dead zone behavior as const
and let
variables.
In other words, JavaScript does not hoist class declarations. Therefore, you must first declare your class before you can access it. Otherwise, the computer will throw an Uncaught ReferenceError
.
Here’s an example:
// Create an object instance from the Name class:const name = new Name();
// Define the Name class:class Name {}
The snippet above throws an Uncaught ReferenceError
because JavaScript does not hoist classes. So, invoking Name()
before its definition is invalid.
2. Classes are functions
The typeof
a class is a function because, under the hood, the class
keyword creates a new function.
For instance, consider the following code:
// Define a JavaScript class:class Bio { // Define two instance class fields: firstName = "Oluwatobi"; lastName = "Sofela"; // Create a prototypal method: showBio() { return `${firstName} ${lastName} runs CodeSweetly.`; }}
// Create a new object instance:const aboutMe = new Bio();
// Check what data type the Bio class is:typeof Bio;
// The invocation above will return: "function"
The computer processes the snippet above like so:
- Create a new function named
Bio
. - Add the class’s instance properties to the newly created function’s
this
keyword. - Add the class’s prototypal properties to the newly created function’s
prototype
property.
3. Classes are strict
JavaScript executes classes in strict mode. So, follow the strict syntax rules when you use classes. Otherwise, your code will throw errors—some of which will be silent errors that are difficult to debug.
4. Avoid the return
keyword in your class’s constructor
method
Suppose your class’s constructor
returns a non-primitive value. In that case, JavaScript will ignore the values of all the this
keywords and assign the non-primitive to the new
keyword expression.
In other words, a constructor’s return
ed object overrides its keyword this
.
For instance, consider the following code:
// Create a new class:class Name { constructor() { this.firstName = "Oluwatobi"; this.lastName = "Sofela"; return { companyName: "CodeSweetly" }; }}
// Create a new object instance:const myName = new Name();
// Check myName's current value:myName;
// The invocation above will return: { companyName: "CodeSweetly" }
// Check firstName's current value:myName.firstName;
// The invocation above will return: undefined
// Check lastName's current value:myName.lastName;
// The invocation above will return: undefined
The new
keyword expression returned only { companyName: "CodeSweetly" }
because JavaScript ignores the constructor method’s this
keywords whenever you use a return
operator to produce an object.
5. A class’s evaluation starts from the extends
clause to its values
JavaScript evaluates your class according to the following order:
1. extends
clause
If you declare an extends
clause, the computer will first evaluate it.
2. Extract the class’s constructor
JavaScript extracts the class’s constructor
.
3. Parse the class’s property names
The computer analyzes the class’s class field names (not their values) according to their order of declaration.
4. Parse the class’s methods and property accessors
JavaScript analyzes the class’s methods and property accessors according to their order of declaration by doing the following:
- Add the prototypal methods and property accessors to the class’s
prototype
property. - Analyze the static methods and property accessors as the class’s own properties, which you can call on the class itself.
- Analyze the private instance methods and property accessors as private properties of the class’s instance object.
5. Parse the class’s property values
The computer analyzes the class’s class field values according to their order of declaration by doing the following:
- Save each instance field’s initializer expression for later evaluations. JavaScript will evaluate the initializer expression during the following periods:
- When the
new
keyword is creating an instance object. - While processing the parent class’s constructor.
- Before the
super()
function call returns.
- When the
- Set each static field’s keyword
this
to the class itself and create the static property on the class. - Evaluate the class’s static initialization blocks and set their keyword
this
to the class itself.