Classes

A class is a blueprint for creating objects that share the same structure and behavior. It provides a convenient way to define the shape of objects and the functionality they possess.

Factory functions

A factory function is a regular function that returns an object. It can create and return new instances of objects without using the new keyword.

function createCar(make, model) {
  return {
    make: make,
    model: model,
    getDetails: function () {
      return `${this.make} ${this.model}`;
    },
  };
}

const car1 = createCar("Toyota", "Corolla");
car1.getDetails(); // => "Toyota Corolla"

Constructors

A constructor function is a special function used with the new keyword to create an instance of an object. It typically uses the this keyword to set properties and methods on the created object. By convention, the name of a constructor function begins with a capital letter.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Adding a method to the prototype of Person
Person.prototype.greet = function () {
  return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
};

When you use the new keyword with a constructor function, JavaScript does several things:

  1. It creates a new empty object.
  2. It sets the prototype of this new object to the constructor function's prototype property.
  3. It calls the constructor function with this set to the new object.
  4. It returns the new object, unless the constructor function returns an object explicitly.
const person1 = new Person("Alice", 30);

person1.name; // => "Alice"
person1.age; // => 30
person1.greet(); // => "Hello, my name is Alice and I am 30 years old.""

Prototype

The prototype is an object that is associated with every function created with the function keyword. When a constructor function is called with the new keyword, the created object's internal __proto__ property (also accessible via Object.getPrototypeOf()) is set to the constructor function's prototype property.

// Checking the prototype
person1.__proto__ === Person.prototype; // => true
Object.getPrototypeOf(person1) === Person.prototype; // => true

This prototype property allows instances created by the constructor function to share methods and properties defined on the prototype. Essentially, all instances inherit from this prototype object and therefore the prototype object defines a class.

Arrow functions do not have a prototype property and therefore cannot be used as constructors. Also, arrow functions inherit this keyword from the context on which they are defined rather than from the instance on which they are invoked, making them useless as constructor's methods.

Note that you can point the same prototype object to two different constructor functions. Then, both constructors can be used to create instances of the same class. You test objects for a membership in a class with the instanceof operator, and the constructor appears on the right.

function Alien() {}
Alien.prototype = Person.prototype;
new Alien() instanceof Person; // => true

Every regular function has a prototype property with the value of an object with a non-enumerable constructor property that points to the function object itself. Therefore, all instance objects inherit a constructor property that refers to their constructor.

let F = function () {};
let p = F.prototype;
let c = p.constructor;
c === F; // => true

Since constructors serve as the public identity of a class, this constructor property gives the class of an object.

let o = new F();
o.constructor === F; // => true

The class keyword

A class can be defined with the class keyword. It is a syntactic sugar over JavaScript's existing prototype-based inheritance, making it easier to define and manage object-oriented structures. Key features:

  1. A class is defined using the class keyword followed by the class name.
  2. A special method called constructor is used to initialize objects. It is called automatically when a new instance of the class is created.
  3. Functions defined within a class describe the behavior of the class instances.
  4. Classes can inherit from other classes using the extends keyword, allowing for shared behavior and properties.
  5. All code within the body of a class declaration is implicitly in strict mode, even if no "use strict" directive appears.
  6. Unlike function declarations, class declarations are not "hoisted".
  7. If your class does not need to do any initialization, you can omit the definition of the constructor function, and an empty constructor function will be implicitly created for you.
  8. The constructor of the class is not named "constructor": a new variable is created with the same name as the name of the class and the value of the defined constructor function is assigned to that variable.
  9. Getters and setters in class declarations work the same as in object literals.
class Animal {
  constructor(name, species) {
    this.name = name;
    this.species = species;
  }

  makeSound() {
    console.log(`${this.name} makes a sound.`);
  }

  move() {
    console.log(`${this.name} is moving.`);
  }
}

Static methods

Static methods in JavaScript are methods that belong to the class itself rather than to instances of the class. Because static methods are invoked on the constructor rather than on any particular instance, it almost never makes sense to use the this keyword in a static method.

class Cat extends Animal {
  constructor(name, age, breed) {
    super(name, age, "Cat"); // Call the parent class constructor
    this.breed = breed;
  }

  makeSound() {
    console.log(`${this.name} meows.`);
  }

  // Static method in Cat class
  static favoriteFood() {
    return "Fish";
  }
}

Cat.favoriteFood(); // => "Fish"

Fields

In JavaScript, fields are properties for classes. The field initialization code appears directly in the class body without the this keyword. Fields can be defined as public, private, or static. Each type of field has specific visibility and access rules that determine how it can be used within the class and by instances of the class:

  • Public fields are accessible from anywhere — both inside and outside the class. They can be modified and read directly from instances of the class.
  • Private fields are only accessible within the class where they are defined. They cannot be accessed or modified directly from outside the class. Private fields are declared using the # symbol.
  • Static fields belong to the class itself rather than to instances of the class.
class User {
  // Public field
  username;

  // Private field
  #password;

  // Static field
  static userCount = 0;

  constructor(username, password) {
    this.username = username;
    this.#password = password;
    User.userCount++;
  }

  // Public method to check the password
  checkPassword(password) {
    return this.#password === password;
  }

  // Public method to change the password
  changePassword(oldPassword, newPassword) {
    if (this.checkPassword(oldPassword)) {
      this.#password = newPassword;
      console.log("Password changed successfully.");
    } else {
      console.log("Old password is incorrect.");
    }
  }
}

// Creating instances of the User class
const user1 = new User("Alice", "password123");
const user2 = new User("Bob", "password456");

user1.username; // => "Alice"
// user1.#password; // SyntaxError
user1.checkPassword("password123"); // => true
user1.checkPassword("wrongpassword"); // => false
user1.changePassword("password123", "newpassword");
// Output: Password changed successfully.
user1.checkPassword("newpassword"); // => true
User.userCount; // => 2

Subclasses

A class B can extend or subclass another class A. In this case, A is the superclass and B is the subclass. Instances of a subclass inherit the methods of its superclass. The subclass can define its own methods, which may override methods of the same name defined in its superclass. You can invoke a method inherited from the superclass by calling super.methodName() anywhere in the body of the subclass.

The subclass constructor typically invokes the superclass constructor in order to ensure that instances are completely initialized. In constructors you are required to invoke the superclass constructor before you can access this to initialize the new object yourself. You can get a reference to the constructor that was invoked with the new keyword by using the new.target.

// Base class AbstractShape
class AbstractShape {
  constructor(name) {
    this.name = name;
    console.log("Initialized AbstractShape constructor for", new.target.name);
  }

  calculateArea() {
    throw new Error("Abstract method");
  }

  calculatePerimeter() {
    throw new Error("Abstract method");
  }

  describe() {
    console.log(`This is a ${this.name}.`);
  }
}

// Subclass Circle extends AbstractShape
class Circle extends AbstractShape {
  constructor(radius) {
    super("Circle");
    this.radius = radius;
  }

  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }

  calculatePerimeter() {
    return 2 * Math.PI * this.radius;
  }

  describe() {
    console.log(`This is a ${this.name} with a radius of ${this.radius}.`);
  }
}

const myCircle = new Circle(5);
// Output: Initialized AbstractShape constructor for Circle
myCircle.describe();
// Output: This is a Circle with a radius of 5.