the-complete-guide-to-javascript-oop-classes

The complete guide to JavaScript OOP Classes

JavaScript implements OOP (class based inheritance) via prototype inheritance: every object inherits properties and methods from its prototype object. The JavaScript language specification ECMAScript 2015 (ES6), introduced classes: special functions which simplify class syntax.

Class Definition

Just as you define function expressions and function declarations, the class syntax can be: class expressions and class declarations. The expression is evaluated at runtime, when the code executes, whereas a declaration is executed beforehand.

Class declaration

The syntax in classes must be written in “strict mode”.

class Flight {
  constructor(number, departure, arrival, price) {
    this.number    = number;
    this.departure = departure;
    this.arrival   = arrival;
    this.price     = price;
  }
}

let depart = new Date("2020-03-01T14:30:00");
let arrive = new Date("2020-03-08T20:00:00");
let flight = new Flight("vy233", depart, arrive, 180);

console.log(Flight.name, typeof Flight, typeof flight);
// Flight function object
console.log(flight);
/* {
	number: "vy233", 
	departure: Sun Mar 01 2020 14:30:00 GMT+0100 (Central European Standard Time), 
	arrival: Sun Mar 08 2020 20:00:00 GMT+0100 (Central European Standard Time), 
	price: 180
	}
*/

Class expression

Class expressions can be named or unnamed.
// unnamed
let Flight = class {
	constructor(number, departure, arrival, price) {
	    this.number    = number;
	    this.departure = departure;
	    this.arrival   = arrival;
	    this.price     = price;
	 }
}
let flight = new Flight("vy233", depart, arrive, 80);
console.log(Flight.name);
// Flight

// named
let Flight = class ComercialFlight{
	constructor(number, departure, arrival, price) {
	    this.number    = number;
	    this.departure = departure;
	    this.arrival   = arrival;
	    this.price     = price;
	 }
}
console.log(Flight.name);

Hoisting

Class declarations cannot be hoisted as function declarations. Using a class before declaring it will throw a ReferenceError.

Constructor

The constructor method is a special method for creating and initializing an object created with a class. Inside the constructor this value equals to the newly created instance. The arguments used to instantiate the class become the parameters of the constructor. If you don’t define a constructor for the class, a default one is created. JavaScript class can have only one constructor. A constructor can use the super keyword to call the constructor of the super class.

Class Fields (properties)

Class fields are variables that hold information and can be attached on the class itself (static) or on the class instance. They can be public (accessible anywhere) or private (accessible only within the body of the class).

Public instance fields

They can be read and updated inside the constructor, methods and outside of the class.
class Airport {
	name = 'el prat';
	location = 'spain';

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

const airport = new Airport('girona');
console.log(airport.name, airport.location); //girona spain
airport.location = 'france';
console.log(airport.location);  // france

Private instance fields

The private fields are accessible only within the body of the class. Encapsulation is the concept of hiding internal data of an object through private fields.
class Airport {
  #code;

  constructor(code) {
    this.#code = code;
  }

  getCode() {
    return this.#code;
  }
}

const airport = new Airport('BCN');
airport.getCode(); // => BCN
airport.#code;     // SyntaxError

Public static fields

These are helpful to define class constants – information specific to the class, so they cannot be accessed by instance class.
class Flight {
	static CABIN_BAGS   = 'cabin';
	static CHECKED_BAGS = 'checked';
	static EXCESS_BAGS  = 'excess';
	price;
	type;
	name;
	luggages;

	constructor(name, type, price, luggages) {
		this.name = name;
		this.type = type;
		this.price = price;
		this.luggages = luggages
	}
}

const flight = new Flight('vy35007', 'domestic', 150, Flight.CABIN_BAGS);
flight.luggages === Flight.CABIN_BAGS; // => true

Private static fields

Private static fields are accessible only within the class (encapsulation).
class Order {
  static #MAX_ORDERS = 2;
  static #num = 0;
  
  customer;

  constructor(customer, price) {
    Order.#num++;
    if (Order.#num > Order.#MAX_ORDERS) {
      throw new Error('Limit reached!');
    }
    this.customer = customer;
    this.price = price;
  }
}

new Order('Violeta', 210);
new Order('Garcia', 55);
new Order('Maria', 170);

Class Methods

Instance methods can access and modify instance data. A method can be private too (prefix its name with #).
class Passenger {
  #cardNumber;
  name;

  constructor(name, cardNumber) {
  	this.name = name;
    this.#cardNumber = cardNumber;
  }

  getName() {
    return this.name;
  }

  #getCardNumber() {
    return this.#cardNumber;
  }

  hasVisa() {
    return /^\d{13, 16}$/g.test(this.#getCardNumber());
  }
}

const passenger = new Passenger('jose', 45223201);
passenger.getName();   // jose
passenger.hasVisa();   // false
passenger.#getCardNumber(); // SyntaxError

Getters and Setters

The name of the getter/setter method cannot be the same as the name of the property. You can prefix with underscore the property name.
class Customer {
	constructor(name) {
	    this._name = name;
	}
	get name() {
	    return this._name;
	}
	set name(cname) {
	    this._name = cname;
	}
}
const user = new Customer("Jorge");
user.name; // calls the getter => jorge
user.name = 'adria'; // calls the setter => {_name: "adria"}

Static methods

The static methods are functions attached directly to the class so they can access only the static fields.
class ScheduledFlight {
	static passengers = [];
	capacity = 55;

	static hasPassenger(name) {
		return ScheduledFlight.passengers.includes(name);
	}

	constructor(name) {
		ScheduledFlight.passengers.push(name)
	}
}
const flight = new ScheduledFlight('John');
ScheduledFlight.hasPassenger('John'); // true

Computed property names

Class allows to compose the property names using brackets:
class Flight {
	['max'+'capacity'] = 50;

	['cancel' + 'Flight']() {
    	return 'ok'
  	}

  	get ['max'+'capacity']() {
  		return this.maxcapacity
  	}
}
const fl = new Flight();
fl.maxcapacity;  // 50
fl.cancelFlight();  // ok

Class Iterators and generators

An object can be iterable if it has a method named with the Symbol.iterator which return an interface called iterator. This iterator must have a method next that returns the next result: an object with a value property and a done property, which should be true when there are no more results and false otherwise.

Regular functions return only one value or nothing. Generators can return (yield) multiple values, one after another, on-demand. They work great with iterables, allowing to create data streams easily.

class FlightReservation {
  id;
  seats = [];

  constructor(seatNum) {
    this.seats.push(seatNum);
  }

  *getSeats() {
    for (let seat in this.seats) {
      yield this.seats[seat];
    }
  }

  [Symbol.iterator]() {
    return this.getSeats();
  }
}

const reservation = new FlightReservation([31, 4, 6, 22]);

// 3 ways to to output the seats array: [31, 4, 6, 22]

// using iterable
for (let seat of reservation)
  console.log(seat);

// using iterator
let iterator = reservation.getSeats();
while (seat = iterator.next().value)
  console.log(seat);

// or spread operator
console.log([...reservation].values().next().value);

Single inheritance (Subclassing)

The classes in JavaScript support single inheritance using the extends keyword. The super keyword is used to call corresponding methods of super class. Note that inside the child constructor you must call super() before other initializations.
class Person {
  name;

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

  getDetails() {
    return this.name
  }
}

class Passenger extends Person {
  constructor(name, priorityBoarding) {
    super(name);
    this.priorityBoarding = priorityBoarding;
  }
}

class Crew extends Person {
	flights = [];

	constructor(name, flights) {
		super(name);
		this.flights = flights;
	}

	getDetails() {
	    return super.getDetails() + ` has ${this.flights.length} flights`
    }
}

const passenger = new Passenger('John', true);
const pilot = new Crew('Martin', ['ab3xd3', 'vy533', 'es990']);

console.log(passenger.getDetails());   // John
console.log(pilot.getDetails());   // Martin has 3 flights     

Person.prototype.constructor ===  Person; // true
Object.getOwnPropertyNames(Person.prototype);  
// returns an array of all properties (non-enumerable also)
// except the ones which use Symbol 
// ["constructor", "getDetails"]

pilot instanceof Person;      // true
pilot.constructor === Crew;   // true
passenger instanceof Person;  // true
passenger.constructor === Person; //false

Multiple inheritance (Mixins)

One way to achieve it in Javascript is by using the Object.assign(target, ...sources) method, which copies enumerable and own properties from a source object to a target object. It uses [[Get]] on the source and [[Set]] on the target, therefore it assigns properties versus just copying or defining new ones.
// create objects to use them as a mixin template
let GroundVehicle = {
  hasFuel() {
    return true
  }
};

let FlyingVehicle = {
  getType() {
    return 'commercial'
  }
};

class Vehicle {
  constructor(name) {
    this.name = name;
  }
}

Object.assign(Vehicle.prototype, GroundVehicle, FlyingVehicle);
const vehicle = new Vehicle('boeing');
vehicle.getType();
vehicle.hasFuel();
What if we change GroundVehicle and FlyingVehicle to classes ? Just create the mixin function which will copy all properties (enumerable and non-enumerable) from source classes to the target one.
class GroundVehicle {
  wheels = 4;
  hasFuel() {
    return true
  }
};

class FlyingVehicle {
  getType() {
    return 'commercial'
  }
};

class Vehicle {
  constructor(name) {
    this.name = name;
  }
}

function mixin(target, ...src) {
    for (let srcClass of src) {
        for (let prop of Object.getOwnPropertyNames(srcClass.prototype)) {
            target.prototype[prop] = srcClass.prototype[prop]
        }
    }
}

mixin(Vehicle, GroundVehicle, FlyingVehicle);
new Vehicle('boeing').getType();   // "commercial"
email-newsletter

Dev'Letter

Professional opinion about Web Development Techniques, Tools & Productivity.
Only once a month!

Any suggestion on what to write next time ? Submit now

Opinions

avatar
550
  Subscribe  
Notify of

Related Posts