Demystifying Babel and ES6

Darren Scerri
8 min readJan 7, 2017

--

This may not be the first time you heard the terms Babel and ES6, or transpilation. You may know what these terms refer to and you may already use these technologies at your workplace or have used them some way or another. We’ll dive a bit deep into these terms and try to open and see what’s inside these black boxes and explain the magic behind Babel.

Let’s start with transpilation. Transpilation is a very important and widely used concept, especially in more recent times. You may have heard of compilers, which are programs that take source-code as input and translates it into a (typically) lower level language, may it be assembly or simply machine code, for example. Transpilers are types of compilers that translate source code into functionally equivalent source code in another language. The main difference between a traditional compiler and a transpiler is that traditional compilers translates code to a lower level of abstraction while a transpiler translates code between languages that operate at more or less the same level of abstraction.

In a more technical sense, all turing-complete languages can be transpiled between each other. A program written in a particular language can theoretically be translated into another program written in another turing-complete language. In other words, for every source code written in a particular programming language, there exists an equally functional source code in all turing-complete languages.

Javascript is 21 years old, so it’s definitely not a new language. Javascript is standardized by ECMAScript, which is a formal language specification that Javascript tracks. ES6 is one version of the spec that was finalised in June 2015. The very latest finalized version is ES7 and was finalised one year later in June 2016.

ES6 is also known as ES2015 (ECMAScript 2015) and is a significant update from ES5 containing major language feature updates. ES6 was a long awaited update after the almost 6 year post-ES5 hiatus. New features are actively being worked on under the ES.Next dynamic name and it looks like new features are to be expected much faster than ever before.

Since Javascript is an interpreted language, programs can only be distributed as source code and therefore an interpreter or a JIT-compiler is needed to execute the program.

These interpreters (more commonly called engines) are found mostly in web browsers or runtime environments (Node.js) and are practically never fully compatible with the latest ECMAScript specification.

V8 is one of the most popular Javascript engines, and is used in Google Chrome and Node.js, along with Chakra (Microsoft) and SpiderMonkey (Mozilla). Although all modern browsers are evergreen (automatically updating without user intervention), Javascript engines still lag behind spec updates, and when engines are updated, there is still a delay until the browsers or runtime environments update the engine that they use. For example, the very latest Node.js version v7.4.0 (at the time of writing) still uses V8 5.4.500.45, while the latest stable V8 version is 5.5.

Full ES5 support is very common, including all non-obsolete browsers and runtime environments. Therefore writing our code in ES5 would be quite a safe bet to ensure that it will run correctly across practically all modern devices. Having learned about transpilation, we can now realise that we can write our program in any ECMAScript version we want as long as we have a transpiler that transpiles the code down to ES5. We can even use non-standardised features (decorators), or any new feature as long as we find a way to transpile it down to ES5.

Babel transpiles our ES6 code down to ES5. So, whenever you write something ES6 specific, this code is translated to functionally equivalent code in ES5; that’s the only magic involved!

Let’s start with the very basic.

Arrow Functions

// ES6
var x = () => {};
// ES5
var x = function() {};
==========================================================// ES6
var x = x => x + 2;
// ES5
var x = function x(_x) {
return _x + 2;
};
==========================================================// ES6
var x = x => x => x + 2;
// ES5
var x = function x(_x) {
return function (x) {
return x + 2;
};
};
==========================================================// ES6
var x = function() {
this.x = 2;
return () => this.x;
};
// ES5
var x = function x() {
// Keep reference to `this`
var _this = this;
this.x = 2;
return function () {
// Use the referenced `this`
return _this.x;
};
};

Since arrow functions do not create their own this context, we need to keep references to this for any subsequent closures.

Block-scoped variables

// ES6
if (x === 5) {
let y = 2;
doSomething(y);
}
if (x === 10) {
let y = 3;
doSomething(y);
}
==========================================================// ES5
if (x === 5) {
var y = 2;
doSomething(y);
}
if (x === 10) {
var _y = 3;
doSomething(_y);
}

To achieve block-scoped variables in ES5, the solution is to change variable names. In the example, we are defining two block-scoped variables y, and using them separately in two different if statement blocks. In the ES5 example, the variables are renamed so they refer to two different variables. Even though the code is semantically different, its behaviour is exactly retained with no side-effects.

Template Literals

// ES6
var world = `world`;
var x = `Hello ${world}! The year is ${new Date().getFullYear()}!`
// ES5
var world = 'world';
var x = 'Hello ' + world +'! The year is ' + new Date().getFullYear() + '!';

This is a very simple transformation, where parameters are extracted and normal string concatenation is used.

Enhanced Object Properties

Shorthand properties and methods

// ES6
var a = 'a', b = 'b';
var x = { a, b };
// ES5
var a = 'a', b = 'b';
var x = { a: a, b: b };
==========================================================// ES6
var x = {
a(b) {
return b + 2;
}
};
// ES5
var x = {
a: function a(b) {
return b + 2;
}
};

Computed properties

// ES6
var x = {
a: 'a',
[new Date().getFullYear()]: 1
};
// ES5
var x = {
a: 'a',
};
x[new Date().getFullYear()] = 1;
// Babel ES5
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var x = _defineProperty({
a: 'a'
}, new Date().getFullYear(), 1);

Things here start to get interesting. Babel has a number of helper functions to perform common tasks. In this case _defineProperty is a helper function that ensures that a defined property is always enumerable, configurable and writable, even if the property already exists. Using _defineProperty here is not explicitly necessary as computed properties can only be used when initializing an object and properties are set by default to be enumerable, configurable and writable:

var x = { a: 'a' };
var d = Object.getOwnPropertyDescriptor(x, 'a');
// true
d.enumerable && d.configurable && d.writable

Spread Syntax

Function Calls and Simpler apply

// ES6
var x = [1,2,3];
var y = (a, b, c) => { return a + b + c; }
var sum = y(...x);
// ES5
var x = [1, 2, 3];
var y = function y(a, b, c) { return a + b + c; };
var sum = y.apply(undefined, x);
==========================================================// ES6
var x = [1,2,3];
var a = {
y: (a, b, c, d, e) => { return a + b + c + d + e; }
};
var sum = a.y(0, ...x, 4);
// ES5
var x = [1, 2, 3];
var a = {
y: function y(a, b, c) { return a + b + c; }
};
var sum = a.y.apply(a, [0].concat(x, [4]));
==========================================================// ES6
var args = [1, 2], last = [4], x = (a, b, c, d, e) => {};
x(0, ...args, 3, ...last);
// ES5
var args = [1, 2], last = [4], x = function x(a, b, c, d, e) {};
x.apply(undefined, [0].concat(args, [3], last));

Spread Properties

// ES6
var x = { a: 1, b: 2 };
var y = { ...x, c: 3 };
// y === { a: 1, b: 2, c: 3 };
// ES5
var _extends = Object.assign; // Babel also polyfills Object.assign
var x = { a: 1, b: 2 };
var y = _extends({}, x, { c: 3 });

Spreading objects is extremely useful and have interesting use-cases. Shallowly copying an object becomes as easy as:

var original = { a: 1, b: 2 };
var copy = { ...original };

For immutability, one can create a copy of an object and modify properties as easy as:

var state = { id: 1, name: 'Bob' };
return { ...state, name: 'Bobby' };

Create an object with default values:

var defaults = { a: 1, b: 2};
var obj = { b: 3, c: 4 };
var merged = { ...defaults, ...obj };
// merged === { a: 1, b: 3, c: 4 }

These are all possible by using Object.assign .

Rest parameters

// ES6
var x = (a, b, ...rest) => { return rest.map(x => a + b + x); }
// ES5
var x = function x(a, b) {
for (var _len = arguments.length, rest = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
rest[_key - 2] = arguments[_key];
}
return rest.map(function (x) {
return a + b + x;
});
};

In this case, rest will be an array of all the arguments passed after a and b. rest is first initialized as an array with the length of all arguments subtracted by the number of fixed arguments (in this case 2). The array is then filled with the rest of the arguments one by one in the for loop.

Classes

Classes might be the most misunderstood concept in ES6. Javascript is a prototype-based language and ES6 classes are simply syntactic sugar over prototypical inheritance. There is no extra magic in classes apart from simple transformations that result in a clearer and simpler syntax, and a more standard way to deal with Object-Oriented Programming.

// ES6
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hello, I'm ${this.name}`;
}
}
// ES5
var Person = function(name) { this.name = name; };
Person.prototype.sayHi = function() {
return 'Hello, I\'m ' + this.name;
};

Babel produces a much more complicated transpilation.

"use strict";var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }var Person = function () {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
}
_createClass(Person, [{
key: "sayHi",
value: function sayHi() {
return "Hello, I'm " + this.name;
}
}]);
return Person;
}();

The base semantics are practically the same but offers a stricter way to define classes in-line with the spec. For example, _classCallCheck ensures that the class is never called as a function (without new).

New Built-In Methods

ES6 also defines a number of additional methods in built-in objects such as String, Array and Object. These can be transpiled simply by defining the function as a polyfill.

// String.prototype.repeat// ES6
"na".repeat(3);
// ES5
String.prototype.repeat = function(count) {
var repeated = '';
for (var i = 0; i < count; i++) {
repeated += this;
}
return repeated;
};

This article should have given you a clearer picture of what is transpilation and why we need it. It’s not just there to ensure support older browsers, but also to write our code in whatever flavour of syntactic sugar we prefer without any worry for compatibility issues. After all, every programming language is just syntactic sugar over 1’s and 0's.

If you’re interested in how compilers/transpilers and Babel works internally or building your own Babel plugin, you should read the official Babel Plugin Handbook.

--

--