Skip to content

ES6 - Classes

Object-Oriented Programming (OOP) is a paradigm that uses objects and their interactions to design and program applications. It models software as a collection of objects, which are instances of classes, communicating through methods. ES6 introduced a class syntax that simplifies creating objects and implementing inheritance, providing a cleaner, more familiar structure for developers accustomed to class-based languages. This syntax is primarily syntactic sugar over JavaScript’s existing prototype-based inheritance.

Let’s revisit fundamental OOP concepts:

  • Object: An instance of a class, representing a real-world entity or an abstract concept. Every object has:
    • State: Described by its attributes (data members or properties).
    • Behavior: Defined by its methods (functions associated with the object).
    • Identity: A unique reference that distinguishes it from other objects.
  • Class: A blueprint or template for creating objects. It encapsulates data (properties) and functions (methods) that define the objects.
  • Method: A function associated with a class or an object, defining its behavior and enabling communication.
  • Encapsulation: Bundling data (properties) and methods that operate on the data within a single unit (a class). ES6+ classes support this through public and private members.
  • Inheritance: Enabling a new class (subclass or child class) to acquire properties and methods from an existing class (superclass or parent class).
  • Polymorphism: The ability of objects of different classes to respond to the same method call in a way specific to their class.

For example, a Car can be an object. Its state includes make, model, color, and currentSpeed. Its behavior includes startEngine(), accelerate(), and brake(). Each Car object is a distinct instance of the Car class.

The class keyword, introduced in ES6, provides a more straightforward way to create classes compared to pre-ES6 constructor functions and prototype manipulation.

Classes can be defined using either a class declaration or a class expression.

class ClassName {
constructor(params) {
// Initialize object properties
}
// Methods
methodName(params) {
// Method logic
}
// Getters and Setters
get propertyName() { /* ... */ }
set propertyName(value) { /* ... */ }
// Static methods
static staticMethodName(params) { /* ... */ }
// Class fields (ES2022+)
publicProperty = 'initialValue';
#privateProperty = 'secret'; // Private class field
static publicStaticProperty = 'staticValue';
static #privateStaticProperty = 'privateStaticSecret';
}
// Anonymous Class Expression
const MyClass = class {
constructor(param) {
this.param = param;
}
// ... other members
};
// Named Class Expression
const AnotherClass = class NamedClassExpression {
constructor(param) {
this.param = param;
}
// The name 'NamedClassExpression' is local to the class body.
// ... other members
};

The class name follows the class keyword. Identifiers for class names typically use PascalCase (e.g., MyClass).

A class definition can include:

  • Constructor: A special method named constructor for creating and initializing objects. A class can have only one constructor. If you don’t define one, a default constructor is provided.
  • Instance Methods: Functions that operate on an instance of the class. Accessed via object.methodName().
  • Static Methods: Functions that belong to the class itself, not to instances. Accessed via ClassName.staticMethodName().
  • Getters and Setters: Special methods to get and set the value of an object’s properties, allowing more control over access.
  • Class Fields (Properties): Variables defined directly within the class body. These can be public or private (using # prefix), and instance or static. Private fields provide true encapsulation.

Note: Class declarations are not hoisted, unlike function declarations. You must declare a class before you can use it.

Example: Declaring a class with modern features

Section titled “Example: Declaring a class with modern features”
class Polygon {
// Public instance field
type = 'Polygon';
// Private instance field
#sides;
constructor(height, width, sides = 4) {
this.height = height;
this.width = width;
this.#sides = sides;
}
// Instance method
getArea() {
return this.height * this.width;
}
// Getter for private field
get sides() {
return this.#sides;
}
// Static method
static describe() {
return 'A polygon is a closed figure made of straight lines.';
}
}
// Example of class expression (anonymous)
const Square = class extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength, 4); // Call parent constructor
this.name = 'Square';
}
// Override method
getArea() {
return this.height * this.height; // or this.width * this.width
}
};

To create an instance of a class, use the new keyword followed by the class name and any arguments for the constructor.

const objectName = new ClassName(arg1, arg2, ...);

Where:

  • The new keyword is responsible for instantiation: it creates a new object, sets its prototype to ClassName.prototype, binds this to the new object within the constructor, and executes the constructor.
  • If the constructor is parameterized, pass the required arguments.
const rectangle = new Polygon(10, 12);
console.log(`Rectangle area: ${rectangle.getArea()}`); // Output: Rectangle area: 120
console.log(`Rectangle sides: ${rectangle.sides}`); // Output: Rectangle sides: 4
console.log(Polygon.describe()); // Output: A polygon is a closed figure made of straight lines.

A class’s instance properties and methods are accessed through an object instance using dot notation (.). Static members are accessed through the class name itself.

// Accessing an instance method
// objectName.methodName();
// Accessing an instance property
// console.log(objectName.propertyName);
// Accessing a static method
// ClassName.staticMethodName();
// Accessing a static property
// console.log(ClassName.staticPropertyName);
class Greeter {
#greeting; // Private field
constructor(message) {
this.#greeting = message;
}
greet() {
console.log(`${this.#greeting}, welcome!`);
}
static getDefaultGreeting() {
return 'Hello';
}
}
// Creating an instance
const myGreeter = new Greeter('Good morning');
myGreeter.greet(); // Output: Good morning, welcome!
// Accessing a static method
console.log(Greeter.getDefaultGreeting()); // Output: Hello
// Trying to access private field from outside (will cause an error or be undefined)
// console.log(myGreeter.#greeting); // SyntaxError in strict mode or if not transpiled properly

In the Greeter class, this.#greeting refers to the private instance field. The greet() method prints this greeting. The static method getDefaultGreeting() can be called directly on the Greeter class.

The instanceof operator checks if an object is an instance of a particular class or a class that inherits from it. It returns true if the object’s prototype chain contains the prototype property of the constructor.

class Animal {}
class Dog extends Animal {}
const myDog = new Dog();
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true (all objects inherit from Object)
const myAnimal = new Animal();
console.log(myAnimal instanceof Dog); // false

ES6 classes support inheritance using the extends keyword. Inheritance allows a subclass (child class) to derive properties and methods from a superclass (parent class), promoting code reuse and establishing a hierarchical relationship.

A subclass inherits all non-private members from its parent. If a subclass has its own constructor, it must call super() before using this. super() calls the parent class’s constructor.

Syntax:

class ChildClass extends ParentClass {
constructor(paramsForChild, paramsForParent) {
super(paramsForParent); // Call parent constructor
// Initialize child-specific properties
this.childProperty = paramsForChild;
}
childMethod() {
// ...
}
}
class Shape {
constructor(color) {
this.color = color;
}
describe() {
return `A shape of color ${this.color}`;
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color); // Call Shape's constructor
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
// Override describe method
describe() {
return `${super.describe()} with radius ${this.radius}`;
}
}
const redCircle = new Circle('red', 5);
console.log(redCircle.describe()); // Output: A shape of color red with radius 5
console.log(`Area: ${redCircle.calculateArea().toFixed(2)}`); // Output: Area: 78.54

In this example, Circle extends Shape. The Circle constructor calls super('red') to initialize the color property via the Shape constructor. Circle also has its own property radius and method calculateArea(), and it overrides the describe() method.

Types of Inheritance Supported:

  • Single Inheritance: A class can extend from at most one parent class. JavaScript/ES6 follows this model.
  • Multilevel Inheritance: A class can be derived from another derived class. For example, class Square extends Rectangle, where class Rectangle extends Shape.

JavaScript does not directly support multiple inheritance (a class inheriting from multiple parent classes). However, patterns like mixins or composition can achieve similar results.

class A {
methodA() {
console.log('Method A from class A');
}
}
class B extends A {
methodB() {
console.log('Method B from class B');
}
}
class C extends B {
methodC() {
console.log('Method C from class C');
}
}
const objC = new C();
objC.methodA(); // Inherited from A
objC.methodB(); // Inherited from B
objC.methodC(); // Own method

A subclass can provide a specific implementation for a method that is already defined in its superclass. This is known as method overriding.

class Printer {
print() {
console.log('Default print output.');
}
}
class TextPrinter extends Printer {
// Override the print method
print(text) {
console.log(`Printing text: ${text}`);
}
}
const myTextPrinter = new TextPrinter();
myTextPrinter.print('Hello, ES6+ Classes!'); // Calls TextPrinter's print method

The super keyword is used to call corresponding methods or access properties of a superclass (parent).

  • In a constructor: super(...args) calls the parent class’s constructor. It must be called before this keyword is used in the subclass constructor if the class extends another.
  • In instance methods: super.methodName(...args) calls the parent class’s method. super.propertyName accesses a parent class’s property (typically via getters/setters if overridden).
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call Animal's constructor
this.breed = breed;
}
speak() {
super.speak(); // Call Animal's speak method
console.log(`${this.name} barks because it's a ${this.breed}.`);
}
}
const myDog = new Dog('Rex', 'German Shepherd');
myDog.speak();
// Output:
// Rex makes a noise.
// Rex barks because it's a German Shepherd.

Further Learning: For more complex OOP patterns and advanced class features, refer to official documentation like MDN Web Docs for JavaScript classes.