Do we need JS-frameworks?

It's 2015 and the next version of ECMAScript, ES6 is feature frozen and will be official in June.

Some examples of what ES6 gives us:

  • Arrow functions
  • Block scope (new modifiers let and const)
  • Classes (with new modifiers: constructor, extends, static, super)
  • Default arguments
  • Modules (import / export)

I'm not going to describe all ES6 features in this post, but only just mention a few that I think we can use to replace what we in ES5 use frameworks for. And theese are Classes, default arguments and Modules (The class modifier is just syntactic sugar for Function.prototype, but with a cleaner syntax).

Classes

Most frameworks have invented their own way to declare "classes". Example:

var PersonModel = Backbone.Model.extend({

    initialize: function(attributes) {
        this.name = attributes.name || 'N/A';
        this.age  = attributes.age || 0;
    }, 

    print: function() {
      console.log('name %s, age %d', this.name, this.age);
    }
});

var person = new PersonModel({ name: 'Kenta', age: 54 });  

In ES6, we could just do:

// Class with inheritence
class PersonModel extends Person {

  // Default arguments  
  constructor(name = 'N/A', age = 0) {
    this.name = name;
    this.age  = age;
    // Call base class
    super(name, age);
  }

  print() {
    console.log('name %s, age %d', this.name, this.age);
  }
}

const person = new PersonModel('Kenta', 54);  
// person cannot be overwritten.

Modules

ES6 modules borrows concepts from Asynchronous Module Definition (AMD) and CommonJS Modules.

Forget about the so called module pattern. No need to write code like this:

// numberService.js
var App = App || {};

App.NumberService = (function() {  
  var NUMBERS = [1, 2, 3];

  // ...
  return {
   getAll: function() { 
     return NUMBERS; 
   }
 };
}());

Or Angulars module system:

module.controller('NumberController', ['NumberService', function(numberService) {  
  // ...
}]);

No need to use any of it in ES6. Instead, we can just write:

// numberService.js
const NUMBERS = [1, 2, 3];

export default class NumberService {  
   getAll() {
     return NUMBERS;
   }
}
// numberController.js
import NumberService from './numberService';

class NumberController {

   // Default argument
   constructor(service = new NumberService()) {
     this.service = service;
   }

   getNumbers() {
     return this.service.getAll();    
   }
}

export default NumberController;  

By using modules with default arguments testing and mocking can be done with ease.

// numberController_test.js
import NumberController from './controllers/numberController';

describe('NumberController', () => {

  let fakeService = { getAll() {} },
  sut;   // System Under Test

  beforeEach(() => {
    sut = new NumberController(fakeService);
  });

  it('can get all numbers', () => {
    spyOn(fakeService, 'getAll'); 

    sut.getNumbers();

    expect(fakeService.getAll).toHaveBeenCalled();
  });
});

Scopes

App.PersonService = {  
  endpoint: 'http://my.api.com/persons',

  fetchAllAdults: function(path) {
    var promise = $.ajax(this.endpoint + path);
    var self = this;
    promise.then(function(persons) {
      return self.filterByAdults(persons || []);
    });

    return promise;
  },

  filterByAdults: function(persons) {
    return persons.filter(function(person) {
      return person.age > 21;
    });
  }
};

1.) The endpoint could be overwritten (this could be fixed with defining it as a private variable outside the App.PersonService).
2.) Need to declare the ugly self = this to avoid getting wrong scope for this. You cannot assign this, but nothing prevents us from assigning/overwriting the self variable.

Same thing in ES6:

import { ajax as $http } from 'jquery';

const ENDPOINT = 'http://my.api.com/persons';

const PersonService = {  
 fetchAllAdults(path) {
   var promise = $http(ENDPOINT + path);

   // No ugly self = this is needed.
   // Arrow functions bind "this" from the containing scope.
   promise.then(persons => this.filterByAdults(persons));
   return promise;
 },

 filterByAdults(persons = []) {
   return persons.filter(person => person.age > 21);
 }
};

// Only export the fetchAllAdults function as default.
export default path => PersonService.fetchAllAdults(path);  
// Same as:
// export default function(path) { PersonService.fetchAllAdults(path); }

In this case, we got smaller amount of code that is easier to read and maintain.

Can I use ES6 today?
Yes!

What about older browsers?
There are several transpilers that translate your ES6 code to ES5. And if you need support for browsers that does not have ES5-support, use ES5-shim.

How?
Have a look at https://babeljs.io/ (previously called 6to5). Its very easy to use with Grunt, Gulp, Brocolli or whatever you use to build your app.

Why is Babel better than google-traceur?
1. Traceur require a runtime (that is quite big, ~ 100kB) to be able to provide all features.
2. Traceur obfuscates your code so its hard to debug.
3. Babel only translates your code from ES6 to ES5.

Summary

So, does ES6 replace all frameworks? No, maybe not. We may still need help with structure, best practices and patterns that we can get from frameworks. But I think that you should consider starting a new project without a framework.

Configure a ES6 transpiler with browserify, and use npm to install the libraries you need for your app instead of using a whole framework.

Why use Angular/Ember/<insert other framework here> if you only need two-way databinding, routing and REST-communication?

Libraries that provide this functionality
Two-way databinding: Vue, React or Knockout
Routing: Page, Crossroads
REST: SuperAgent, whatwg-fetch

More on that, in a future post.

Links

[1] Using ECMAScript 6 today
[2] The power of ECMAScript 6
[3] ES6 Specification, RC 1