Sugarcoat me, baby.

Overview

Standing for ECMAScript 2015, ES6 was a turning point for JavaScript. It was released in June 2015, and it brought a lot of new syntax improvements and functionalities that were non-available or cumbersome (to say the least). Through the use of babel we can compile our ES6 assets into backwards-compatible code that run on almost all browsers.

In our Design System, we make extensive use of the new functionalities brought by ES6, so make sure you know all about it to use and collaborate with the Design System!

If you want to know more, head to the ECMA site to find more information about the latest news about ECMAScript or check the resources at MDN or W3Schools to improve your skills.

ES6 Highlights

Variables: let & const

Instead of using var to declare all your variables, now you have available let and const too.

The main difference in between the new declarations and the old is the scope.

let

The closest of the two new declarations to the old var the key difference is that let has a scope. It can only be accessed locally from within the block level it was defined in.

const

Equal to let it has a local scope, but in this case we are declaring a variable to have an immutable value. Once declared, it cannot be changed.

Extended parameter handling

Default parameter values

A nice little addition that makes it much easier to set default values for parameters when declaring our functions. For those cases in which maybe the value is not passed when calling the function:

function inc( number, increment = 1 ) {
  return number + increment;
}
console.log( inc( 2, 2 )); // 4
console.log( inc( 2 ));    // 3

You can even execute a function expression to set a default parameter, not being limited to primitive values:

function getDefaultIncrement() {
  return 1;
}

function inc( number, increment = getDefaultIncrement()) {
  return number + increment;
}
console.log( inc( 2, 2 )); // 4
console.log( inc( 2 ));    // 3

Rest parameter

This parameter allows us to represent an indefinite number of arguments as an array, condensing all of them as a single element:

// ES6 Syntax
function sum( ...numbers ) {
  var result = 0;
  numbers.forEach( number => {
    result += number;
  });
  return result;
}
console.log( sum( 1 ));             //  1
console.log( sum( 1, 2, 3, 4, 5 )); // 15

// Old syntax
function sum() {
  var numbers = Array.prototype.slice.call( arguments ),
    result = 0;
  numbers.forEach( function( number ) {
    result += number;
  });
  return result;
}
console.log( sum( 1 ));             //  1
console.log( sum( 1, 2, 3, 4, 5 )); // 15

Spread operator

While the rest operator let us accept an iterable as a parameter, the spread operator let us use iterables such as arrays or strings be expanded in places where zero or more arguments are expected:

// ES6 Syntax
function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum(...args)); // 6

// We can even mix standard arguments and spread operator
function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2];
console.log(sum(...args, 3)); // 6

// Old syntax
function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum.apply(undefined, args)); // 6

Enhanced object properties

Object literals make it easy to quickly create objects with properties inside the curly braces. To create an object, we simply notate a list of key: value pairs delimited by comma. ES6 makes the declaring of object literals concise and thus easier.

Property shorthand

Prior to ES6, if we wanted to initialize a property in an object using object literals, we would have to assign the value of the parameters to its corresponding keys, repeating a lot. Since ES6, the property gets checked, and if the key has a corresponding variable name, it assigns the value of that variable to the property. Note that if no variable has the same name as the property key defined, we'll get an error.

// ES6 Syntax
function getLaptop( make, model, year ) {
  return {
    make,
    model,
    year
  }
}

getLaptop( 'Apple', 'MacBook', '2015' ); // {make: "Apple", model: "MacBook", year: "2015"}

// Old syntax
function getLaptop( make, model, year ) {
  return {
    make: make,
    model: model,
    year: year
  }
}

getLaptop( 'Apple', 'MacBook', '2015' ); // {make: "Apple", model: "MacBook", year: "2015"}

Computed property names

There are two ways we can access an object property: dot notation and bracket notation. With the later one, we can access the property using expressions. Computed property names allows us to write an expression between square brackets instead of the regular property name. Whatever that expression evaluates to, will become the property name.

// Example 1
var name = 'make';
const laptop = {
  [ name ]: 'Apple'
}

console.log( laptop.make ); //"Apple"

// Example 2
var name = 'make',
  i = 0;
const laptop = {
  [ name + ++i ]: 'Apple',
  [ name + ++i ]: 'Dell',
  [ name + ++i ]: 'HP'
}

console.log( laptop.make1 ); //"Apple"
console.log( laptop.make2 ); //"Dell"
console.log( laptop.make3 ); //"HP"

In our code, the last example has values that are computed and concatenated to get the name of the property.

Method properties

As with the shorthand for properties ES6 also brings a shorthand to make the creation of methods more concise.

// ES6 Syntax
function getLaptop(make, model, year) {
  return {
    sayModel() {
      return model;
    }
  }
}

getLaptop("Apple", "MacBook", "2015").sayModel(); //"MacBook"

// Old syntax
function getLaptop( make, model, year ) {
  return {
    sayModel: function() {
      return model;
    }
  }
}

getLaptop( 'Apple', 'MacBook', '2015' ).sayModel(); //"MacBook"

Notice how the new syntax is clearer, removing the : function() making our code shorter and more readable.

Destructuring assignment

Destructuring is a convenient way of extracting multiple values from data stored in (possibly nested) objects and Arrays. It can be used in locations that receive data (such as the left-hand side of an assignment).

Array matching

If we want to extract data using arrays, it's very simple using a destructuring assignment.

const iterable = [ 'a', 'b', 'c' ];
const [ x, y ] = iterable;
// x = 'a'; y = 'b'

Notice that the variables are set from left to righ. So the first variable gets the first item in the array, the second variable gets the second variable in the array and so on. Taking this into account, if we wanted to skip one variable in the array, we just put another comma in between, and the empty space will get ignored.

const iterable = [ 'a', 'b', 'c' ];
const [ x,, y ] = iterable;
// x = 'a'; y = 'c'

Maybe we want to put certain values in certain variables, and the rest of them as an array inside another value, we can do so using the spread operator like in the example:

const iterable = [ 'a', 'b', 'c' ];
const [ x, ...y ] = iterable;
// x = 'a'; y = [ "b", "c" ]

Object matching

Following up what we've seen about destructuring arrays, we can do the same with objects:

// ES5
var person = { name: 'Sarah', country: 'Nigeria', job: 'Developer' }

var name = person.name;
var country = person.country;
var job = person.job;

console.log( name );    //"Sarah"
console.log( country ); //"Nigeria"
console.log( job );     //Developer"

// ES6
var person = { name: 'Sarah', country: 'Nigeria', job: 'Developer' };

var { name, country, job } = person;

console.log( name );    //"Sarah"
console.log( country ); //"Nigeria"
console.log( job );     //Developer"

When using object literal destructuring assignment without a declaration (i.E. we want to assign the values to variables created before assignation) we need parenthesis () around the assignment statement. This is because the {} on the left hand side is considered a block and not an object literal.

The variables in the object on the left hand side should have the same name as the key on the object we're destructuring. If the names are different we'll get undefined.

// Error
var person = { name: 'Sarah', country: 'Nigeria', job: 'Developer' };

var { name, friends, job } = person;

console.log( name );    //"Sarah"
console.log( friends ); //undefined

// Correct way
var person = { name: 'Sarah', country: 'Nigeria', job: 'Developer' };

var { name: foo, job: bar } = person;

console.log( foo );   //"Sarah"
console.log( bar );   //"Developer"

We can even make use of default values to fill in the gaps that we may encounter and avoid the undefined error.

var person = { name: 'Sarah', country: 'Nigeria', job: 'Developer' };

var { name = 'myName', friend = 'Annie' } = person;

console.log( name );    //"Sarah"
console.log( friend );  //"Annie"

Fail soft

Destructuring is fail-soft, meaning that values not found return undefined instead of an error.

Map / Set

JavaScript has always been very spartan regarding its data-structures. At the beginning it only had arrays, things improved when objects were introduced with ES5. Now, with ES6 we get:

  • Map
  • Set
  • WeakMap & WeakSet

Map data structure

The Map structure stores key/value pairs, much like Objects do, but with some differences:

  • The keys of an Object are String or Symbol, whereas they can be any value for Map including functions, objects and any primitive.
  • The keys in a Map are ordered.
  • You can get the size of a Map easily with its size property.
  • A Map is directly iterable.
  • An Object has a prototype, and inherits its default keys, that could interfere with new keys if we are not careful.
let map = new Map();

map.set('foo', 123);
map.set('bar', false);
map.get('foo') // 123

map.size // 2
map.has('foo') // true
map.delete('foo') // true
map.has('foo') // false

map.clear()
map.size // 0

You can iterate through the contents of a Map using either a forEach or a for ... of loop:

let map = new Map([[0, 'val1'], ['1', val2], [{ key: 2 }, val3]]);

map.forEach((value, key) => {
    console.log(value, key)
  }
);

Also, you have access to .entries(), .keys() and .values() if you want to iterate directly on those methods.

To check if a value is stored in your map, you can use .has() method. As well as the .delete() method to delete an entry or the .clear() one to empty the whole Map:

let map = new Map([[0, 'val1'], ['1', val2], [{ key: 2 }, val3]]);

map.has('1')
map.size // 3

map.delete('1')
map.size // 2

map.clear()
map.size // 0

Set data structure

Similar to Map, Set is a collection of value, only in his case we are talking about unique values. There can be no duplicated entries (or values, in case we're not using the keys).

let set = new Set([1, 1, 1, 2, 5, 5, 6, 9]);
console.log(set.size); // 5!

It's methods are the same as in Map.

WeakMap & WeakSet

Both Map and Set data structures are very persistent in memory, preventing the garbage-collector from removing them from memory. In certain scenarios this can lead to problems and memory leaks. For this reason, WeakMap and WeakSet work as they're strict counterparts, but do not prevent the garbage-collector from removing them, freeing memory.

New methods

These are some of the new methods available in ES6. These methods aim to simplify and standardize some familiar scenarios that we encounter when working with JavaScript.

Object property assignment

The Object.assign() method is used to copy the values of all own enumerable properties from one or more source objects, to a target one, which will be returned. We can use this to clone or merge objects. The properties in the target will be overwritten by properties on the sources if they have the same key.

// Base code
Object.assign(target, ...sources)

// Cloning object
let obj = { name: 'Krunal', surname: 'Lathiya' };
let cloneObj = Object.assign({}, obj);
console.log(cloneObj) // Object { name: "Krunal", surname: "Lathiya" }

// Merge objects
let o1 = { a: 21 };
let o2 = { b: 22 };
let o3 = { c: 24 };

Object.assign(o1, o2, o3);
console.log(o1); // Object { a: 21, b: 22, c: 24 }

If we need deep-cloning, we need to look for other functions, as assign will not work for this purpose.

Array element finding

.find()

With Array.find() we can retrieve the value of the first element in an array that satisfies the provided function.

// Base code
let array1 = [ 5, 12, 8, 130, 44 ],

let found = array1.find( element => {
  return element > 10;
});

console.log( found ); // expected output: 12
.findIndex()

If instead of the value, that we can get with Array.find() we need the index in the array, we use Array.findIndex() instead. The system is the same: we get the index of the first value that satisfies the provided testing function.

// Base code
let array1 = [ 5, 12, 8, 130, 44 ];

let found = array1.findIndex( element => {
  return element > 10;
});

console.log( found ); // expected output: 1

For the same purpose of finding an index we could use the method Array.indexOf() but with this last method we can only provide a value (not a function). This is perfectly valid for simple primitives, but not for objects, that need a more complex matching system.

String searching

These are a series of methods created specifically to search inside strings.

.startsWith()

This method determines wheter a string starts with a provided set of characters or not, returning true or false accordingly. We can also specify, as a second parameter, the position at which we want to start testing, being 0 (start) the default.

const str1 = 'Saturday night plans';

console.log( str1.startsWith( 'Sat' )); // expected output: true

console.log( str1.startsWith( 'Sat', 3 )); // expected output: false
.endsWith()

Similiar to the previous one, but here we test if the string ends with the provided characters. The system works in the same way:

const str1 = 'Cats are the best!';

console.log( str1.endsWith( 'best', 17 )); // expected output: true

const str2 = 'Is this a question';

console.log( str2.endsWith( '?' )); // expected output: false
.includes()

And to finish, we have the .includes() method. Similar to the previous ones, here we can search for a provided string inside another string, returning true or false accordingly.

const sentence = 'The quick brown fox jumps over the lazy dog.';

let word = 'fox';

console.log( `The word "${word}" ${sentence.includes( word ) ? 'is' : 'is not'} in the sentence` );

// expected output: "The word "fox" is in the sentence"

Number type checking

.isNaN()

The .isNaN() method checks whether a value is NaN and its type is Number. This function is necessary because when checking NaN with equality operators, we will always get false. The difference with the isNaN() function is that the method does not coerce the value into a number, so only values that are of the type Number will return true.

Number.isNaN( 123 ) //false
Number.isNaN( -1.23 ) //false
Number.isNaN( 5 - 2 ) //false
Number.isNaN( 0 ) //false
Number.isNaN( '123' ) //false
Number.isNaN( 'Hello' ) //false
Number.isNaN( '2005/12/12' ) //false
Number.isNaN( '' ) //false
Number.isNaN( true ) //false
Number.isNaN( undefined ) //false
Number.isNaN( 'NaN' ) //false
Number.isNaN( NaN ) //true
Number.isNaN( 0 / 0 ) //true
.isFinite()

We can use the .isFinite() method to check whether a value is of type Number and it's a finite number. It returns true or false accordingly.

Number.isFinite( 123 ) //true
Number.isFinite( -1.23 ) //true
Number.isFinite( 5 - 2 ) //true
Number.isFinite( 0 ) //true
Number.isFinite( '123' ) //false
Number.isFinite( 'Hello' ) //false
Number.isFinite( '2005/12/12' ) //false
Number.isFinite( Infinity ) //false
Number.isFinite( -Infinity ) //false
Number.isFinite( 0 / 0 ) //false

Number truncation

We can use the number truncation Math.trunc() method to get the integer part of a floating number. The argument passed to this method will be converted to Number type implicitly.

console.log( Math.trunc( 13.37 )); // expected output: 13

console.log( Math.trunc( 42.84 )); // expected output: 42

console.log( Math.trunc( 0.123 )); // expected output: 0

console.log( Math.trunc( -0.123 )); // expected output: -0

Number sign determination

This function let us know if a number is positive or negative, returning a 0 if the given value is 0.

console.log( Math.sign( 3 )); // expected output: 1

console.log( Math.sign( -3 )); // expected output: -1

console.log( Math.sign( 0 )); // expected output: 0

console.log( Math.sign( '-3' )); // expected output: -1

Modules: import & export

One of the main features of ES6 is the inclusion of modules: now we can write our code splitted in different files, and import them into a single file that will be used in our site. Even though this option is natively supported by modern browsers, in our case we still compile the assets Through webpack, profitting to run some other optimizations.

Simple import/export

If you must export only one function, object, variable or constant, you can use export default. Then you import it where needed.

/**
* @file module.js
*/

export default function() {
  console.log( 'Hello world!' );
}

/**
* @file main.js
*/
import sayHello from 'file-to-export';

sayHello(); //console.log( 'Hello world!' );

Multiple import/export

Instead of exporting/importing a single element, you can export multiple functions, objects, variables or constants through the use of a new shorthand introduced with ES6:

/**
* @file module.js
*/
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

/**
* @file main.js
*/

//import only square and diag
import { square, diag } from './module';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

//ES5 syntax
var square = module.square, diag = module.diag;

If you want to learn more about modules, head to MDN.

Arrow functions

Another shiny new syntax feature of ES6

// Old Syntax
function oldOne() {
  console.log('Hello World..!');
}

// New Syntax
var newOne = () => {
  console.log('Hello World..!');
}

There are two parts to the new syntax: The first is just declaring a variable and assigning it the function. The second part is declaring the body of the function.

Expressions vs. statements

The main difference between function expressions and function statements is that the first functions get evaluated, while the statements doesn't. This mean we can get and use a function expression as a value, while the function statement performns an action, without creating a value.

In JavaScript we mainly work with function statements (conditionals like if or switch, loops, etc...) but there are benefits using function expressions: using them as closures, as arguments to other functions or as Immediatly Invoked Function Expressions (IIFE).

// Expression example: Here we use the arrow function as an expression, meaning that the function evaluates and that value acts as the parameter for the map method.
odds  = evens.map(v => v + 1)

// Statement example: In this case, the arrow function does not evaluate, it just performs an action: pushing the argument to an existing array if some conditions are met.
nums.forEach(v => {
  if (v % 5 === 0) {
    fives.push(v)
  }
})

To learn all about arrow functions and its multiple benefits, head over to MDN, and check their awesome resources.

For of loop

In addition to the for in loop, now ES6 brings us the possibility to use a for of loop. This kind of loop iterates over all values of an iterable object.

let arr = [2,3,4,1];

for (let value of arr) {
  console.log(value);
}

// Output:
// 2
// 3
// 4
// 1

To learn more about this loop, check MDN resources.

👁 Warning!
Even though these loops are amazing and offer a very good performance, they should be avoided as of today due to backwards compatibility with older browsers.

Promises

Promises are a clean way to implement async programming in JavaScript, and was introduced in ES6. Before the promises, we needed to implement callbacks, but the process is totally different.

var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('foo');
  }, 300);
});

promise1.then(function(value) {
  console.log(value);
  // expected output: 'foo'
});

console.log(promise1);
// expected output: [object Promise]

Chaining promises

Promise chaining is the very reason we have promises in the first place. It's a proper way to tell JavaScript the next thing to do after an asynchronous task is donde, avoiding the callback hell.

const promise = job1();

promise

  .then( function ( data1 ) {
    console.log('data1', data1);
    return job2();
  })

  .then( function ( data2 ) {
    console.log( 'data2', data2 );
    return 'Hello world';
  })

  .then( function ( data3 ) {
    console.log( 'data3', data3 );
  });

function job1() {
  return new Promise(function ( resolve, reject ) {
    setTimeout( function () {
      resolve( 'result of job 1' );
    }, 1000 );
  });
}

function job2() {
  return new Promise( function ( resolve, reject ) {
    setTimeout( function () {
      resolve('result of job 2');
    }, 1000 );
  });
}

If you want to know all the details about this new feature, check MDN complete guide about it.

Classes

JavaScript's take on classic languages concept of classes was called prototypal inheritance, and since ES6 it was 'sugarcoated' into JavaScript classes , or templates that we can use to create objects when needed.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

const p = new Rectangle( '100', '100' );

The constructor method is called each time the class object is initialized. In our example, creating a 100x100 rectangle.

Class inheritance

To create a class inheritance, use the extends keyword. The extends keyword is used in class declarations or class expressions to create a class as a child of another class. A class created with a class inheritance inherits all the methods from another class:

class Car {
  constructor(brand) {
    this.carname = brand;
  }
  present() {
    return 'I have a ' + this.carname;
  }
}

class Model extends Car {
  constructor(brand, mod) {
    super(brand);
    this.model = mod;
  }
  show() {
    return this.present() + ', it is a ' + this.model;
  }
}

mycar = new Model('Ford', 'Mustang');

To access the parent class, we call the super() method in the constructor method, getting access to the parent's constructor method and its properties and methods.

Getters & setters

The get and set syntaxes binds objects to a functions. In the first case, the function will be called when said property is looked up, while on the second case it will be called when there's an attempt to set that property.

// Getter example:
var obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length == 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  }
}

console.log(obj.latest); // expected output: "c"

// Setter example:
var language = {
  set current(name) {
    this.log.push(name);
  },
  log: []
}

language.current = 'EN';
language.current = 'FA';

console.log(language.log); // expected output: Array ["EN", "FA"]

Check MDN to get all documentation about JavaScript classes.

Template literals

String literals or template literals are a new ES6 feature that allows us to interpolate expressions in a much simpler way than before. Now, instead of "adding" the strings, we can embed them into another string as long as it's declared with back-ticks (`).

let chunk = 'example';

let old = 'This is an ' + chunk + ' of the old syntax.';

let new = `This is an ${ chunk } of the new syntax.`;

To discover more about template literals, go to MDN and check their resources.