Hello, my name is Seth Bergman. I am a

Full Stack Engineer

focused on helping companies scale. I love learning about software architecture, containers, open source programming and automation. I use technologies that drive innovation, speed up development and provide continuous delivery of awesome software.

ES6 Learning Notes - Classes

Classes

In the old way of writing classes, a constructor function and a prototype object that all instances would inherit from had to be created.

This looked something like this:

var Employee = function() {

};

Employee.prototype = {  
  doSomething: function(){
    return "done!";
  }  
};

var e = new Employee();  

This syntax sucks. The new ES6 syntax to do the same thing is like this:

class Employee {  
  doSomething() {
    return "complete!";  
  }

  doSomethingElse() {
    return "also complete!"  
  }
}

var e = new Employee();  
e.doSomething(); // "complete!"  

This doesn't require the use of the function keyword. Behind the scenes, the same thing is happening. Commas aren't required between method definitions, and semicolons are optional.

Currently we only have hardcoded strings in our objects, so let's introduce state and constructor functions.

The Constructor

Managing state consistently requires some initialization logic for an object, that is implemented with a constructor. The constructor function is automatically invoked when you say new <classname>, and inside the constructor you can use the implicit this reference to manipulate the object and store instance state.

class Employee {  
  constructor(name) {
    this._name = name;
  }

  doSomething() {
    return "complete!";
  }

  getName() {
    return this._name;
  }
}

let e1 = new Employee("Taylor");  
let e2 = new Employee("Charles");

e1.getName(); // "Taylor"  
e2.getName(); // "Charles"  

Getters and Setters

class Employee {

  get name() {
    return this._name.toUpperCase;
  }

  set name(newValue) {
    this._name = newValue;
  }
}
let e1 = new Employee("Taylor");  
let e1 = new Employee("Charles");

e1.name; // "Taylor"  
e2.name; // "Charles"

e1.name = "Chris"; // sets e1's name to "Chris"  

Using get in this manner, we are now able to use the cleaner call e1.name.

You don't have to supply a setter if you don't want the object's properties to be changed.

It is also possible for the constructor to call the setter:

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

set name(newValue) {  
  this._name = newValue;
}

Inheritance

The 3 Pillars of Object Oriented Programming:
1. Encapsulation
2. Polymorphism
3. Inheritance

In ES6, it is easy to specify an inheritance relationship.

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

  get name() {
    return this._name;
  }

  set name(newValue) {
    this._name = newValue;
  }
}

Say we want to create an employee, and each employee "is-a" person. The extends keyword handles our inheritance.

class Employee extends Person {  
  doWork() {
    return `${this._name} is working`; // uses ES6's literal syntax
  }
}

let p1 = new Person("Taylor");  
let e1 = new Employee("Chris");  
e1.doWork(); // "Chris is working"  

Often there will be additional pieces of information that will need to be stored in an object. For example, our Employees should have a title...

super

super allows you to forward a call from a child class's constructor to the superclass's constructor and forward along the required parameters.

class Employee extends Person {  
  constructor(name, title){
    super(name);
    this._title = title;
  }  

  get title() {
    return this._title;  
  }
}

You could use this._name = name; in the derived class constructor, but this isn't the appropriate approach, since there could be validation logic, etc.

super can also be called in other ways. For example, If a Person does work, they do it for free. If they are an Employee, they are paid:

class Person {

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

  get name() {
    return this._name;
  }

  set name(newValue) {
    if(newValue) {
      this._name = newValue;
    }
  }

  doWork() {
    return "free";
  }
}


class Employee extends Person {

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

  get title() {
    return this._title;
  }

  doWork() {
    return "paid";  
  }
}

let e1 = new Employee("Taylor", "Developer");  
let p1 = new Person("Charles");

p1.doWork(); // "free"  
e1.doWork(); // "paid"  

We have overwritten the doWork() method. We can also overwrite the toString() method:

class Person {  
  .
  .
  .
  toString() {
    return this.name;
  }
}

We can cycle through an array of people to see if they're working:

let makeEveryoneWork = function(...people) {  
  var results = [];
  for (var i = 0; i < people.length; i++){
    results.push(people[i].doWork());
  };
  return results;
}

makeEveryoneWork(p1, e1); // ["free", "paid"]  

Note that the makeEveryoneWork function doesn't care if someone is a person or an employee, because both have a doWork method. To adjust to code to have a check that the object passed in has a doWork function, use:

if(people[i].doWork) { ... }  

or

if(people[i] instanceof Person) { ... }