ES6 and Beyond - Part 3: Classes

by Paul Selden — on  , 

cover-image

This is part 3 in a series of posts that will help familiarize you with many of the features of the recently-approved ECMAScript 6 specification (aka ES2015, ES Harmony, ESNext) as well as go through some of the proposed changes for ES6+/ES7. It focuses on providing realistic examples of how your code changes before and after applying the new ES6 and beyond features.

Other posts in the series:

  1. Part 1: Sugar
  2. Part 2: Promises, generators, and async

Part 3 - Classes

This post will focus on a controversial but important feature that was introduced in ES6: Classes (via the class keyword), and how they are improved in ES7 and even in TypeScript.

JavaScript has ways to create classes by using functions and prototypal inheritance, so why do we need a class keyword? We’ll look through some examples and see how using ES6 classes will make your code cleaner and perhaps more important – compatible with the larger JavaScript library ecosystem. With class you can avoid choosing from the hundreds of possibly incompatible class libraries.

Basic Classes

Let’s take a look at how very basic classes can be implemented:

Before

A simple ES3/ES5 class:

// a simple ES3/ES5 class
function User(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

User.prototype.getFullName = function (){
  return this.firstName + ' ' + this.lastName;
}

var user = new User('Paul', 'Selden');
user.getFullName(); // 'Paul Selden';
user instanceof User; // true

Pretty straightforward. There are already a couple of strange things though for people unfamiliar with JavaScript (or even people familiar with it who have not tried to use classes):

  1. You use a function as your class definition, and then instantiate it with new. So what happens if you forget to use new – the constructor runs, but it’s run just like a normal function so that it will return undefined. If you’re not using the returned value immediately (and seeing that it’s undefined) you may end up creating a hard to find bug as you pass undefined around.
  2. You add new methods to it by extending its prototype, not the function itself. If you had typed User.getFullName = ... it would have just created a new function on the User object, but user.getFullName() would not work.

After

Now let’s see how a basic class looks like in ES6:

class User {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new User('Paul', 'Selden');
user.getFullName(); // 'Paul Selden';
user instanceof User; // true

Also pretty straightforward and familar to those who use object-oriented languages. Instead of a function, you create a class with a constructor method that is evaluated when it is called with new.

The strange parts in the before example are now gone:

  1. It looks and acts like a class. By reading it you see immediately that it should be used as a class instead of a function. What happens here if you forget to call new? Well with ES6 classes that’s actually an error! You know right away that something is wrong with your code by failing fast.
  2. Your class methods are part of your class definition. (Side note: classes use the object-shorthand for functions – you don’t use the function keyword to define them.) It’s much easier to see what methods are available to your class when it is kept together. Even though ES6 classes use the protoype under-the-hood (User.protoype.getFullName is defined!), that is an implementation detail that the user does not need to know.

Static Methods and Instance Properties

ES6 Classes can also have static methods. ES7 also has a proposal for static properties and instance methods.

Before

function User(){
  this.type = 'user'; // instance property
  User.totalUsers++;
}

User.createUser = function() { // static function
  return new User();
};

User.totalUsers = 0; // static property

var user = new User();
user.type; // 'user';
User.totalUsers; // 1

Again, we see that we have to break up the class definition and the static methods.

After

class User {
  type = 'user'; // instance property, only available in ES7

  static createUser() { // static function, available in ES6
    return new User();
  }

  static totalUsers = 0; // static property, only available in ES7

  constructor() {
    User.totalUsers++;
  }
}

var user = new User();
user.type; // 'user';
User.totalUsers; // 1

With ES6 and ES7 class features, you can keep the entirety of the class definition together.

Inheritance

What good would a class be if it didn’t support inheritance? This is why you’ll see so many different third party libraries implementing object-oriented programming in different ways. Thankfully ES6 came along and standardized it so libraries that rely on classes (like React, Angular2) can be built.

Simple extends

After seeing how much boilerplate is needed to do inheritance without a third party library, you may understand why some standardization was needed.

Before

function User(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

User.prototype.getFullName = function() {
  return this.firstName + ' ' + this.lastName;
};

function TitledUser(title, firstName, lastName) {
  User.call(this, firstName, lastName); // calling constructor of super class, now this.firstName and this.lastName are assigned
  this.title = title;
}

TitledUser.prototype = Object.create(User.prototype, { // extending TitledUser's prototype with User's prototype and adding methods
  getFullName: {
    value: function () { // overriding getFullName method
      return this.title + ' ' + User.prototype.getFullName.call(this); // calling superclass method
    }
  },
  constructor: { // making sure instanceof works correctly
    value: TitledUser
  }
});

var user = new TitledUser('Mr.', 'Paul', 'Selden');
user.getFullName(); // 'Mr. Paul Selden';

It works, but it’s not pretty. Extending the class is done by creating a copy of the super class’s prototype and extending it with its own implementation via Object.create. This is a bit clunky, but the stranger parts happen when you need to call in to the super class’s constructor and methods, you must use the class definition or prototype and use .call or .apply and pass in the current context. This is just noise and hides what you’re trying to do.

After

class User {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

class TitledUser extends User {
  constructor(title, firstName, lastName) {
    super(firstName, lastName); // calling super constructor
    this.title = title;
  }

  getFullName() { // override method
    return `${this.title} ${super.getFullName()}`; // call super class's method
  }
}

const user = new TitledUser('Mr.', 'Paul', 'Selden');
user.getFullName(); // 'Mr. Paul Selden';

This is much cleaner. There’s much less boilerplate and it does exactly what you’d expect it to do without having to mess with constructors. Note: the super constructor must called in a Child class’s constructor before you use this, otherwise it will throw an error.

You can continue to extend this class as many times as you want. However, you cannot extend from more than one class (multiple inheritance).

class User {}
class TitledUser extends User {}
class SuperTitledUser extends TitledUser {} // okay!

class Animal {}
class FourLegged {}
class Dog extends Animal, FourLegged {} // nope, can't do this

Public vs Private in classes

As of ES7, there is no first-class support for private methods or properties, but there are several tricks that you can use.

Before

// we want to make 'age' private
function User(firstName, lastName, age) {
  this.firstName = firstName;
  this.lastName = lastName;

  this.canVote = function () {
    return age >= 18; // age was not exposed publicly, so it cannot be modified externally
  };

  this.growOlder = function () {
    age++; // internally we can still modify age
  };
}

var user = new User('Paul', 'Selden', 17);
user.canVote(); // false
user.growOlder();
user.canVote(); // true
user.age; // undefined, can't access it because we did not expose it publicly

You’ll notice that our age variable is now accessible via closure from the internal functions, but not accessible from outside code. Unfortunately, in order to do this we had to not add our methods to our prototype, which has a performance concerns (because each new user has brand-new allocated methods instead of a shared prototype) and also will cause issues with inheritance because we can’t just extend the prototype and expect all the super methods to work.

After

With ES6, although there is no public/private modifiers, we can use Symbol to create properties that no outside code can access unless you export the symbols for them.

const _age = Symbol('age'); // as long as we don't expose this symbol here, external code cannot use it to access our user's age

class User {
  constructor(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this[_age] = age; //
  }

  canVote() {
    return this[_age] >= 18;
  }

  growOlder() {
    this[_age]++;
  }
}

var user = new User('Paul', 'Selden', 17);
user.canVote(); // false
user.growOlder();
user.canVote(); // true
user.age; // undefined

This is better for performance reasons since the functions go on User’s prototype, but it’s still a little strange to use Symbols like that every time we want to access the age.

Wouldn’t it be great if we could still use this.age inside this class and mark it as private so that external code cannot use it?

TypeScript classes

If you’re using TypeScript, you can get all of the features in ES6 and planned for ES7 and get the benefit of private, public and protected keywords that other languages have. Plus, you get optional typing! Win/win!

class User {
  private ssn; // can't access this externally

  constructor(public firstName, public lastName, private age) {
    // when you mark a constructor's parameters as public or private, they are automatically added to 'this'
    // this.firstName, this.lastName are exposed publically, this.age is private
    this.ssn = generateSSN(); // assume we're giving the user a new social security number
  }

  canVote() {
    return this.age >= 18;
  }

  growOlder() {
    this.age++;
  }
}

var user = new User('Paul', 'Selden', 17);
user.canVote(); // false
user.growOlder();
user.canVote(); // true
user.age; // compile error, Property 'age' is private and only accessible within class 'User'.
user.ssn; // compile error, Property 'ssn' is private and only accessible within class 'User'.

With TypeScript classes we can get even better encapsulation than ES6/ES7 can provide, and we even eliminate some boilerplate code by automatically assigning constructor properties to this. Note: this code will never be able to run in the first place, since it will be a TypeScript compile error, not a runtime error, when you try to access private member variables.

Conclusion

Like it or not, classes in JavaScript are here to stay. They drastically simplify inheritance and provide library authors some common ground to build on. With the addition of TypeScript and public/private modifiers, classes get even more powerful.

In the next part of “ES6 and Beyond” we’ll be checking out a powerful ES7 and TypeScript feature: Decorators.

References and further reading

To learn more about JavaScript classes, try Exploring JS: Classes, or

If you’d like to play with ES6/ES7 classes, try it out here: Babel playground

If you’d like to play with TypeScript classes, try it out here: TypeScript playground