April 14, 2016

JavaScript is all grown up, or is it?

JavaScript is all grown up, or is it?

JavaScript has received its first major update since 2009. Find out what ES6 features will have the biggest impact on your codebase using real-world examples and get the tooling to get started in all 3 flavours: Gulp, Grunt and Webpack. > ES6 stands for ECMAScript6, which is the latest version of language specification JavaScript is derived from.
Back in December of 2015, I took the lead to re-write the 3D engine that powers the Design Center application at a previous employer. We used ThreeJS to render room layouts and provide an easy way to swap out fixtures, materials and flooring. Behind the scenes, we run algorithms that rank room layouts based on constraints most favourable for users.

At the time, I was just starting to explore ES6; I did a short talk about its features at our local PyDev meet-up group. I found this to be a perfect opportunity to put it to the test and the result was one of the most fun projects I've ever worked on.

This article will cover ES6 features that had a biggest impact on our codebase as well as real-world examples of how we test, document and run ES6 code in the browser. Everything discussed is also available on github, in Gulp, Grunt and Webpack flavours.


Code features

ES6 is the biggest update to JavaScript yet, including many new features and programming patterns we've been waiting for. Check out https://github.com/lukehoban/es6features for a full list.

Here are the three features that made the biggest impact on our codebase:

Classes and OOP

For many developers coming from Java or C++, organizing code in JavaScript is a complete nightmare. Although the concept of Object Oriented Design (OOP) has existed for some time via prototypal inheritance and the Web is full of articles on the matter, the syntax always sucked, paving the way for a plethora of frameworks that "simulate" OOP, further fragmenting the space and making understanding JavaScript sources a daunting task, especially for newcomers.

Although it's mostly just syntactical sugar, the new syntax for Classes provides a familiar, consistent way to structure JavaScript code across browsers and a great way to attract new developers as well as give us veterans some sanity.

...the new syntax for Classes provides a familiar, consistent way to structure JavaScript code across browsers...

While working on our 3D engine, I realized that ThreeJS and ES6 are perfect for one another. Here's an example why:

/* This class is used to render a wall object in the scene. */
class Wall extends THREE.Object3D {
  
  constructor(params) {
    super();
    this.add(this.drawMesh(params));
    this.add(this.drawCollisionMesh(params));
  }

  // method
  drawMesh(params) {
    return new THREE.Mesh(geometry, material);
  }
  
  // getter
  get material() {
    return this.children[1].material;
  }

  ...
} 

Not only is this easy to read and debug, but it ensures every object that renders in the scene is a ThreeJS 3D object, and not containers with unnecessary references. This gives me full control and makes the scene hierarchy predictable.

Arrow functions

I've seen new developers run for the hills when introduced to how JavaScript handles scope and this. I've also seen countless .bind() interview questions thrown at potential recruits, often work-arounds for the fundamental issue with JavaScript scope. Having to not deal with this since ES6 has been quite amazing.

With traditional functions, every new function defined its own this which proved to be annoying to say the least. Developers are forced to keep references to this via self or use .bind() functions to have access to scope outside said function.

Arrow functions on the other hand lexically bind the this value, which means when working with classes, this will always be the class instance.

I've also seen countless interview questions about .bind() thrown at potential recruits, often as work-arounds for the fundamental issue with JavaScript scope.

Here's an example of our Ghost class, which allows users to see through walls as they rotate the room.

class Ghost {
    constructor(targets) {
        this.targets = targets;
        MessageBus.on('controls:change', function() {
            this.see(this.targets); // error: this.targets is undefined
        });
    }
...

Prior to ES6, you'd need to bind to this, or both see and targets would come back undefined:

class Ghost {
    constructor(targets) {
        this.targets = targets;
        MessageBus.on('controls:change', function() {
            this.see(this.targets);
        }.bind(this));
    }
...

At first, this may seem harmless, but in my experience I've seen this pattern cause memory leaks that are hard to track. Incorrect (ab)use of .bind() also causes performance issues in React. (More on this in a future article)

In ES6, we address this issue by using arrow functions.

class Ghost {
    constructor(targets) {
        this.targets = targets;
        MessageBus.on('controls:change', ()=> {
            this.see(this.targets);
        });
    }
...

Keep in mind, there are valid use-cases for using .bind() - for example, to bind the context of class methods; if you need to bind, do so in the constructor, and reference the bound function throughout the class, or if you're living on the edge: use the @autobind ES7 Decorator

More on Arrow Functions on MDN.

Module Imports

While we've gotten very comfortable using require() to load modules the nodejs way with browserify, ES6 has added a number of cool features to make this even more convenient. We can now pluck individual functions from our Constants file, instead of requesting the full file and referencing members we want to use.

import { wallThickness, wallOpacity } from './Constants';

Utils now exports a number of constants, instead of an Object.

// file: Constants.js
export const wallThickness = 50;
export const wallOpacity = 0.1;

## Code Formatting & Linting Linting ensures our codebase is consistent in terms of formatting, legibility and best practices. AirBnB has good set of linter options for ESLint that we're beginning to follow (except we're sticking to 4 spaces).

Testing and Mocks

Although I've come across many articles that talk about ES6 testing, very few provide real-world examples where we need to mock out functionality in order to provide a dependable testing environment. On top of the popular Jasmine + Karma combo, we use babel-plugin-rewire to rewire import calls in order to stub them out for testing.

For example, here's a snippet of a utility function that loads a texture by uri:

// file: Utils.js
export default function getMaterialByUri(uri) {
    return new Promise((resolve, reject) => {
        // ...make AJAX calls and resolve the promise with the texture
}

As the product loads, it needs to fetch the texture first, so we need to stub out getMaterialByUri in order to unit test Product.js.

// file: Product.js
import { getMaterialByUri } from './Utils';

...
load(params) {
    return getMaterialByUri(params.textureUri)
        .then(() => {
           //... apply texture
        })
}

This is how it is done using the rewire plugin:

// file: Product.spec.js
import Product from './Product';

describe('Product', ()=> {
    it('loads model and applies texture', ()=> {
        // rewire the call to immediately return a sample Material
        Product.__Rewire__('getMaterialByUri', ()=> Promise.resolve(sampleMaterial));
        Product.load();
        //... more tests
    });
});

This allows us to mock imports to classes, functions, etc, making testing straight-forward. A similar sample is included in the starter-kit download below.


Documentation

ESDoc is fairly new documentation generator. It is under active development and we haven't encountered any major issues using it with our ES6 code.


Browser Support

Although browser vendors are eager to implement all of ES6's features, ES6 code needs to be transpiled down into ES5 in order to run in current browsers. Babel is the most popular tool for the job - it transpiles ES6 code to ES5 and generates source-maps so that you can set break-points and debug the code you write, not the code the browser runs. Neat huh?

The starter kit at the bottom of this article sets up Babel with source-maps so you don't have to worry about it.


Issues

As much as I love the new features, there's a few issues I've come across that may have to wait for native support (unless I've missed a solution somewhere out there).

  • this is undefined in Chrome's Web Inspector when placing break-points in ES6.

this is undefined

  • imports are also undefined when debugging:
    imports undefined

  • When referencing bundled code, .default has to be added:

let instance = new LayoutRenderer.default();

The (Near?) Future

Although ES6 (or ES2015) hasn't been around that long, ES7 (or ES2016) is already on the way with a few very interesting proposals. One I look forward to is the concept of Decorators.

Decorators give us a way to add metadata to a function, class or property, which is used to change its behaviour.

The easiest example is making a property read-only:

class Foo {
   constructor() {
   }
   @readonly
   name = 'Foo';
}

More complex use cases include auto-binding functions to this, and even adding mixins! Let's make Foo draggable and resizable:

@mixin(draggable, resizable)
class Foo {
   constructor() {
   }
}

As exciting as this is, decorators are still in early stages, and are yet to be implemented in Babel 6. If you really can't wait any longer to use an awesome decorator library such as core-decorators, add the babel-plugin-transform-decorators-legacyplugin to your Babel workflow.


Downloads

The starter kits below include all of the features mentioned in the article, plus a few goodies, such as browser-sync for live-reload.

Webpack + Babel starter kit.
Gulp + Browserify + Babel starter kit.
Grunt + Browserify + Babel starter kit.

Hope this helps you get up and running - feel free to leave comments, concerns and questions below.