Skip to content
sergio edited this page Dec 30, 2020 · 28 revisions

An alternative way to structure our code, Object Orientation. By understanding and using OO, we can design our code to be easier to read, understand and change.

Object Orientation grants us the ability to write code that structurally establishes the relationships between data and functions.

  • Easier to Change
  • Offers Better Data Control
  • Easy to Replicate
  • Improves Understanding by Adding Meaning

The class keyword is just syntactic sugar, or a nice abstraction, over JavaScript's existing prototypal object structure. no function keyword is used for instance methods.

class Square {
  constructor(sideLength) {
    this.sideLength = sideLength;
  }
  // notice how the instance method doesn't have the `function` keyword
  area() {
    return this.sideLength * this.sideLength;
  }
}

let square = new Square(5);
square; // => Square { sideLength: 5 }
square.sideLength; // => 5
square.area(); // => 25

In class methods, this acts similar to Ruby's self keyword. this can be used to refer to properties of an instance, like name and age, or methods of an instance (this.sayName()).

You can also access the properties of an instance with:

square[`sideLenght`]
// => 5
square[`sideLenght`] = 13

So to use variables to call properties.


Static Methods

Static methods are class level methods - they are not callable on instances of a class, only the class itself. These are often used in 'utility' class that encapsulate a set of methods but don't need to be made into instances. For example, we could write a CommonMath class that stores a series of math related methods. For static methods, this references the class. This means that you can call a static method from within another static method of the same class using this.

class CommonMath {
  static triple(number) {
    return number * number * number;
  }

  static findHypotenuse(a, b) {
    return Math.sqrt(a * a + b * b);
  }

  static doubleTriple(number) {
    return this.triple(number) * 2
  }
}

let num = CommonMath.triple(3);
num; // => 27
let c = CommonMath.findHypotenuse(3, 4);
c; // => 5

This sort of class might be useful in many different situations, but we don't ever need an instance of it.


Getters and Setters

get

In modern JavaScript get has been introduced for methods which serve the specific purpose of retrieving data from an instance.

class Square {
  constructor(sideLength) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength * this.sideLength;
  }
}

let square = new Square(5);
square.sideLength; // => 5
square.area; // => 25

If you try to use this.area(), you'll receive a TypeError. area is no longer considered a function!

//BAD PRACTICE!
class Square {
  constructor(sideLength) {
    this.sideLength = sideLength;
    this.area = sideLength * sideLength;
  }
}

This is valid code, but what we've done is load our constructor with more calculations.

The main benefit to using get is that your area calculation isn't actually run until it is accessed.

If included in the constructor, an expensive process will be called every time a new instance of a class is created.

Since properties can change, any values dependent on them should be calculated based on the current property values, otherwise we will run in to issues like this:

//also:
class Square {
  constructor(sideLength) {
    this.sideLength = sideLength;
    this.area = sideLength * sideLength;
  }
}

let square = new Square(5);
square.area; // => 25
square.sideLength = 10;
square.area; // => 25

set

class Square {
  constructor(sideLength) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength * this.sideLength;
  }
}

square.sideLength = 10;
square.area; // => 100

We can change area by changing sideLenght however we can't assign area a new value. To make area fully act like a real property, we create both get and set methods for it:

class Square {
  constructor(sideLength) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength * this.sideLength;
  }

  set area(newArea) {
    this.sideLength = Math.sqrt(newArea);
  }
}

let square = new Square(5);
square.sideLength; // => 5
square.area; // => 25
square.area = 64;
square.sideLength; // => 8
square.area; // => 64

area became a pseudo-property.

From the outside, it looks like a property is being set, but behind the scenes, we can define what we want to happen, including applying conditional statements:

set area(newArea) {
  if (newArea > 0) {
    this.sideLength = Math.sqrt(newArea)
  } else {
    console.warn("Area cannot be less than 0");
  }
}

Private Properties

All properties are accessible from outside an instance, as we see with square.sideLength, as well as from within class methods (this.sideLength).

class Transaction {
  constructor(amount, date, memo) {
    this.amount = amount;
    this.date = date;
    this.memo = memo;
  }
}

The date, amount and memo properties represent fixed values for each instance when a Transaction instance is created and probably shouldn't be altered. However, it is still possible to change these properties.

there is no official way to make a property private - all class and object properties are exposed as we see above. One common convention, however, is to include an underscore at the beginning of the property name to indicate those properties are not intended to be accessed from outside the class:

class Transaction {
  constructor(amount, date, memo) {
    this._amount = amount;
    this._date = date;
    this._memo = memo;
  }
}

Implementing private properties is planned in future versions of JavaScript, and will use a # symbol to indicate a property is private.


Private Properties with get and set

You may remember, though that in JavaScript, properties are exposed. That is, any property can be reassigned from outside. Here is where get and set really shine. With get and set, we can define the 'public' facing methods for updating a 'private' property.

In this Student class, we've set up a pseudo-property, firstName, which refers to a 'private' property _firstName:

class Student {
  constructor(firstName, lastName) {
    this._firstName = this.sanitize(firstName);
    this._lastName = this.sanitize(lastName);
  }

  get firstName() {
    return this.capitalize(this._firstName);
  }

  set firstName(firstName) {
    this._firstName = this.sanitize(firstName);
  }

  capitalize(string) {
    // capitalizes first letter
    return string.charAt(0).toUpperCase() + string.slice(1);
  }

  sanitize(string) {
    // removes any non alpha-numeric characters except dash and single quotes (apostrophes)
    return string.replace(/[^A-Za-z0-9-']+/g, '');
  }
}

let student = new Student('Carr@ol-Ann', ')Freel*ing');
student; // => Student { _firstName: 'Carrol-Ann', _lastName: 'Freeling' }

student.firstName = 'Hea@)@(!$)ther';
student.firstName; // => 'Heather'

When to Use Methods Over get and set

normal instance methods can do everything that get and set can do. All class methods and properties are exposed for use 'publicly'. Using get and set is purely for design purposes to produce better organized, easier to understand classes.

We can use get and set whenever we are handling input or output of a class. We are, in essence, creating the public interface of the class. get and set methods are the ways in which other classes and code should utilize this class.

Using this design, all remaining methods can be considered private. They don't deal with input and output; they are only used internally as helper methods.

Using a setter, you can call a function each time the value of a pseudo-property is changed, making sure all data on a class instance is consistent. Using a getter, you can return a computed value as though it is a property!


extends

extends is used in class declarations to create a class which is a child of another class.

class Pet {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    return `${this.name} says ${this.sound}!`
  }
}

class Dog extends Pet {
  // inherits constructor from Pet
}

class Cat extends Pet {
  // inherits constructor from Pet
}

class Bird extends Pet  {
  // inherits constructor from Pet
  fly() {
    return `${this.name} flies away!`
  }
}

let dog = new Dog("Shadow", "woof");
let cat = new Cat("Missy", "meow");
let bird = new Bird("Tiki", "squawk");

dog.speak(); // Shadow says woof!
cat.speak(); // Missy says meow!
bird.speak(); // Tiki says squawk!
bird.fly(); // Tiki flies away!

super allows a child class to access a parent's constructor from within a child's constructor. It also allows a child class to access methods and properties from a parent class, but as most of these are already inherited, this is only useful when modifying static methods from the parent class.

class Pet {
  constructor(name) {
    this._name = name;
    this._owner = null;
  }

  get name() {
    return this._name;
  }

  get owner() {
    return this._owner;
  }

  set owner(owner) {
    this._owner = owner;
  }

  get speak() {
    return `${this.name} speaks.`;
  }
}

// Inherits from Pet
class Dog extends Pet {
  constructor(name, breed) {
    super(name); /* new */
    this.breed = breed;
  }
}

let creature = new Pet('The Thing');
let dog = new Dog('Spot', 'Foxhound');

dog;
// => Dog { _name: 'Spot', _owner: null, breed: 'Foxhound' }

To understand why we use super above think of how you would have told otherwise to send the name variable to all of those methods inherited that use name in Pet. super allows you to access anything in the parent class. In a child class constructor, super is used as a method and calls the parent class constructor before continuing with the child. This lets us extend a parent's constructor inside a child. If we need to define custom behavior in a child constructor, we can do so without having to override or ignore the parent.

Outside of the constructor, the super keyword is also used, but this time, as an object not as a function. In this case below, instance methods and properties are already inherited, so using super.owner will be the same as using this.owner:

// Inherits from Pet
class Dog extends Pet {
  constructor(name, breed) {
    super(name); /* new */
    this._breed = breed;
  }

  get breed() {
    return this._breed;
  }

  get info() {
    if (super.owner) {
      return `${this.name} is a ${this.breed} owned by ${super.owner}`;
    }
    return `${this.name} is a ${this.breed}`;
  }
}

let charlie = new Dog('Charlie B. Barkin', 'Mutt');

dog.info;
// => 'Charlie B. Barkin is a Mutt'

let lady = new Dog('Lady', 'Cocker Spaniel');
lady.owner = 'Darling Dear';

lady.info;
// => 'Lady is a Cocker Spaniel owned by Darling Dear'

Using super as an object is useful in situations where a parent class contains a static method that we want to expand on in a child class:

class Pet {
  constructor(name) {
    this.name = name;
    this._owner = null;
  }

  get owner() {
    return this._owner;
  }

  set owner(owner) {
    this._owner = owner;
  }

  static definition() {
    return `A pet is an animal kept primarily for a person's company.`;
  }
}

// Inherits from Pet
class Dog extends Pet {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  static definition() {
    return (
      super.definition() + ' Dogs are one of the most common types of pets.'
    );
  }
}

let creature = new Pet('The Thing');
let dog = new Dog('Spot', 'foxhound');

Pet.definition();
Dog.definition();

In Dog, we are able to use super.definition() to access that static method, then add to it, in this case, extending the definition to specifically reference dogs.

So super is mainly used to expand in a child class the definitions of a parent class. JavaScript provide super for directly working with a parent class constructor and inherited methods.


Liskov's substitution principle: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. In terms to JavaScript inheritance, an instance of a parent class should be replaceable with an instance of a child class.

Properties and methods that exist on the parent will never be modified in any child. Child classes can expand upon what they inherited, adding methods or extra properties, do not modify what they inherited.

If you need to modify a child's inherited properties or methods, why are we inheriting them in the first place?

The benefit of following this principle is that no matter how complicated inheritance gets, you can always assume that whatever a parent class has, a child class will have too. Looking at a parent class should give us some insight into the functionality of any children, grandchildren, great grandchildren, etc...

A child class may include more properties or use the data differently. If you do find that a child class needs to overwrite a method or property it inherited from its parent, consider other options - perhaps don't inherit at all.

In general, when dealing with inheritance, the fewer levels of inheritance, the better. If you've got great grandparent, grandparent, parent and child classes, it can be difficult to figure out which class contributes what to a child. It also makes our code more difficult to change. You may not be able to modify code on a grandparent class without fundamentally changing how a parent or child class functions. Too much inheritance can make our code inflexible.


Languages that allow you to "extend what's already there" are Prototypal object systems such as languages like C, Lisp, Self, and JavaScript (natively).

  • Class model: When we say "tree", our minds build a foggy rough idea of what all trees are like. There is a something defining tree-ness. (Plato)
  • Prototypal model: Everything that is, is like a something else with more specification added (Aristotele)

Creating instances from a function (Constructor functions), when we invoke the constructor function with new before it, you can imagine an imaginary JavaScript Object being copied for use in the User function, the freshly-created, new context is changed. Constructor functions creations identify their constructor as the constructor name. The type of (typeof) its prototype attribute is Object.

function User(name, email) {
	this.name = name;
	this.email = email;
	this.sayHello = function() {
		console.log(`Hello everybody, my name is ${this.name} whom you've been
mailing at ${this.email}!`);
	};
}

let lauren = new User('lauren', 'lauren@example.com');
lauren.name //=> [6] "Lauren"
lauren.sayHello(); //=> Hello everybody, my name is lauren whom you've been mailing at lauren@example.com!
> typeof lauren
'object'
> lauren.constructor
[Function: User]

Properties vary from instance to instance, but functions are always the same we should not instantiate them for each instance. That is why the above method to create instances is not efficient. We would like to tell all instances of User that they have a shared place to find methods. That place is called the "prototype."

function User(name, email) {
  this.name = name;
  this.email = email;
}

User.prototype.sayHello = function() {
  console.log(`Hello everybody, my name is ${this.name}`);
};

let sarah = new User('sarah', 'sarah@example.com');
let lauren = new User('Lauren', 'lauren@example.com');

sarah.sayHello(); //=> // "Hello everybody, my name is sarah!"
lauren.sayHello(); //=> // "Hello everybody, my name is Lauren!"

User.prototype;
// {sayHello: ƒ, constructor: ƒ}
typeof User.prototype;
// object

To prove the efficiency of sharing methods via prototype:

lauren.sayHello === sarah.sayHello; //=> true

The following code will create only 1 definition of razzle()

function Wizard(name) { this.name = name }
["radegast", "gandalf", "harry potter", "merlin"].map( n => new Wizard(n.charAt(0).toUpperCase() + n.slice(1) ))
Wizard.prototype.razzle = function() {
    console.log(`${this.name} razzles you!`)
}

These days, most OO JS will be written using class and the ES2016 standard, but you will likely encounter and might well be interviewed on questions around the Prototypal model.


Class Variables

class Clax {
    varinst = 423;
    static variClass() {
      return 42
    };
};

Static function/variable is accessible from the Class

Clax.variClass();
//=> 42

You can also assign Class variables after the Class has been defined

Clax.foo = 'jees';
Clax.foo  //now accessible from the Class
//=> "jees"
c = new Clax;
c.varInst
=> 423

c.foo
=> undefined

Clax.varInst
=> undefined

From within a class method that variable foo can be accessed as this.constructor.foo (or Clax.foo).

These class properties (foo) would not usually be accessible from to the class instance. i.e. Clax.foo gives 'bar' but new Clax().foo is undefined

If you want to also have access to your class variable from an instance, you'll have to additionally define a getter:

class Clax {
    get foo() {
        return this.constructor.foo;
    }
}

Clax.foo = 'jees';

To get all the instances of a Class you can create a static array and store it on your constructor function:

MyClass.allInstances = [];
MyClass.allInstances.push(this);

References:

Clone this wiki locally