Getting Started with ES6 Classes

Though ES6 hasn’t been completely adopted anywhere quite yet (keep up on that), you can take advantage of many of its features today using libraries like Traceur and the ES6 module loader polyfill or using iojs with various –harmony flags (David Walsh has a nice writeup on all the harmony feature flags in io.js). In this post I hope to help you get started working with ES6 classes. If you are already familiar with OOP in ES5, feel free to skip straight to the ES6 classes. Download this ES6 starter to try the examples (you’ll need to add your code to the script tag, as the console doesn’t like much of the new syntax quite yet).

ES5 Classes

Javascript implements the object-oriented paradigm on top of its prototype system, rather than having a class system. Object-oriented javascript is achieved by defining a function and attaching properties & methods to its prototype.

// define the constructor 
function API(baseUri) { 
  if(window === this) {
    throw new Error('Missing "new" keyword in API class instantiation');
  }
  this.baseUri = baseUri || ''; 
}

API.prototype = {
  get: function(path, params) {
    return $.get(this.baseUri + path, params || {});
  },
};

var fb = new API('http://graph.facebook.com/');
fb.get('rhettandlink').then(function(response){
  console.log(response);
});

That’s the very basics of defining a ‘class’ in Javascript. Inheritance is a bit less straightforward if you are coming from a classical OOP background:

// define your child class constructor
function Facebook() {
  // this is equivalent to parent::construct(), super(), etc. and is a necessary step
  API.call(this, 'https://graph.facebook.com');
}
// inherit the prototype of the parent class
Facebook.prototype = Object.create(API.prototype);
// ensure the proper constructor is used
Facebook.prototype.constructor = Facebook;

We could go on about OOP in Javascript for hours, but many better writers have covered it before, I suggest: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript

ES6 Classes

So what are ES6 classes? They are mostly syntactic sugar on top of what we just covered, prototype-based ES5 classes. You don’t need to use the keyword function anymore to define methods or classes, properties can only be set within methods instead of attached directly to the prototype, we get extends for cleaner inheritance, super for calling parent methods and much more. Here is our API base class rewritten with ES6:

class API {
  constructor(baseUri) {
    this.baseUri = baseUri || ''; 
  }
  get(path, params) {
    return $.get(this.baseUri + path, params || {});
  }
}
// no change in usage
var fb = new API('http://graph.facebook.com/');
fb.get('rhettandlink').then(function(response){
  console.log(response);
});

We now have declared a base class, it is important to note that base classes differ from derived classes, more on that shortly. Notice the lack of commas separating methods, if you separate your methods with a comma you will get a syntax error (however, you can use semicolons between method definitions). As expected, the constructor method will be called anytime we create a new Object from our class definition and just like ES5 functions, it will implicitly return this; also just like ES5 classes, you can return something other than this from the constructor. Unlike ES5, we cannot accidentally call API() without the new keyword, ES6 classes will throw an error. Let’s go a little more in depth on constructors. ES6 introduces the spread operator, and it will be helpful to understand that to see how the constructors are implicitly defined. From MDN:

The spread operator allows an expression to be expanded in places where multiple arguments (for function calls) or multiple elements (for array literals) are expected.

The spread operator is more syntactic sugar, it essentially gives us a nice shortcut to someFunction.apply(context, *arguments*) and can perform operations like:

// dynamic arguments
someFunction(...arguments);

// add an unknown number of elements into an array literal
var lastLetters = ['x', 'y', 'z'];
console.log(['a', 'b', 'c', ...lastLetters]);

// array destructuring
var [a, b, ...rest] = [1, 2, 3, 4, 5, 6];
console.log(rest); // [3, 4, 5, 6];

Spread… not just best gun in Contra anymore. Moving on: the default constructor for a base class is: constructor() {}. If you want your class to have properties, then you need to define a constructor and initialize the properties there. The default constructor for a derived class is:

constructor(...args) {
  super(...args);
}

Note the super method being called, in many ways this is like what was done in ES5:

function ChildClass() {
  ParentClass.call(this);
}

However, when defining a derived class with a custom constructor, you must call super() prior to attempting to access “this”. This is due to a difference between ES5 and ES6 classes in how the object is initialized. In ES5, the instance object (i.e. `this`) is created in the operand of `new`, so the object exists and is ready to use before the parser enters your constructor/prototype; in ES6, the instance object is created in the base constructor.

Derived Classes

Creating a derived or child class is as simple as `extending` the base class:

class Facebook extends API {
  constructor(apiKey) {
    super('https://graph.facebook.com/');
    this.apiKey = apiKey;
  }
  get(profile, params) {
    // log, rate limit, whatever 
    return super.get(profile, params)
  }
}

var fb = new Facebook(API_KEY);
fb.get('rhettandlink').then(function(response) {
  console.log(response);
});

Other Class Goodies

Static methods

The static keyword added before method names will make them accessible directly on the class (rather than from an instantiated object). Here is an example of a class with static methods:

class Querystring {
   // turn querystring key value pairs into an object
   static parse() {
     var qs = location.search.substr(1).split('&');
     var map = {};
     
     qs.forEach(function(pair) {
       // ES6 Destructuring https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
       var key, val = pair.split('=');
       map[key] = val;
     });
     return map;
   }
   // turn object into querystring format
   static stringify(obj) {
     // ES6 string templates https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings
     return Object.keys(obj || {}).reduce(function(prev, cur) {
       return `${attr}=${encodeURIComponent(obj[attr])}&`;
     }, "");
   }
}

var parsed = Querystring.parse();
var qs = Querystring.stringify(parsed);

console.log(parsed, qs);

Getters and Setters

Much the same as ES5 getters and setters, you can setup getters and setters for your properties or allow for computed properties on your classes. Here’s an example of getters and setters in action:

class FizzBuzz {
  constructor() {
    this.fizz = 'fizz';
    this.buzz = 'buzz';
  }
  // computed attribute
  get fizzbuzz() {
    return this.fizz + this.buzz;
  }
  set fizzbuzz(value) {
    var split = value.split(' ');
    this.fizz = split[0];
    this.buzz = split[1];
  }
}

var test = new FizzBuzz();
console.log(test.fizzbuzz); // "fizzbuzz"
test.fizzbuzz = "hello world";
console.log(test.fizzbuzz); // "helloworld"

Gotchas

Unlike functions, classes are not hoisted; this means that you have to define your classes prior to attempting to instantiate them. I’ll add to this list as I come across new trickiness.

Conclusion

I hope this was a helpful introduction, if you have any questions please comment. If you want to work with ES6 classes all the time, we’re hiring.

Leave a Reply