Categories:

Top five features in JavaScript ES6 Worth Mastering

Posted: Sept 20th, 2017

JavaScript ES6 adds a slew of new features to the JavaScript language, some more groundbreaking and widely applicable than others. Things like ES6 Classes for example, while novel, merely adds syntactical sugar on top of existing ways to create classes in JavaScript, while features like Generators, while extremely powerful, are reserved for more specialized tasks.

From working on various JavaScript related projects over the last 12 months, these are the top five features of ES6 I've found to be indispensible, as they genuinely simplify how common tasks are accomplished in JavaScript. Your top 5 may differ from mine, which if they are I encourage you to share in the comments section at the end.

Here we go!

  1. Arrow Functions

  2. Promises

  3. Async Functions

  4. Destructuring

  5. Default and Rest Parameters

1) JavaScript Arrow Functions

One of my favourite additions in ES6 JavaScript isn't so much a new feature, but a refreshing new syntax set that puts a smile on my face each time I use it. I'm talking about Arrow functions, which  provide a supremely elegant and minimalistic way to define anonymous functions in JavaScript.

In short, an Arrow function drops the "function" keyword, and uses an arrow (=>) to separate the parameter portion of the function from the function BODY of an anonymous function:

(x, y) => x * y;

This is equivalent to:

function(x, y){
	return x * y;
}

or:

(x, y) => {
	var factor = 5;
	var growth = (x-y) * factor;
}

which is identical to:

function(x, y){
	var factor = 5;
	var growth = (x-y) * factor;
}

Arrow functions also removes a key source of error when working with traditional anonymous functions, which is the value of the "this" object inside the function. With Arrow Functions, "this" is lexically bound, which is just a fancy way of saying its value is bound to the parent scope, and never changes. If an arrow function is defined inside a custom object "countup", the value of "this" points to "countup"- there's no guesswork involved. For example:

var countup = {
	counter: 15,
     
	start:function(){
		window.addEventListener('click', () => {
			alert(this.counter) // correctly alerts 15 due to lexical binding of this
		})
	}
};
 
countup.start();

Compare that to a traditional anonymous function, where the value of "this" changes depending on the context it's defined in. The result when trying to reference this.counter in the above case would return undefined, a behaviour that would probably confound many not familiar with the intricacies of dynamic binding. With Arrow Functions, the value of "this" is always predictable and easy to deduce.

For the full skinny on Arrow Functions, please see "Overview of JavaScript Arrow Functions".

2) JavaScript Promises

JavaScript ES6 Promises streamlines the way asynchronous tasks are handled, a task that plays out inside most modern web applications. Instead of relying on callback functions- popularized by JavaScript frameworks such as jQuery - JavaScript Promises uses a central, intuitive mechanism to keep track of and respond to asynchronous events. Not only does it make debugging asynchronous code so much easier, but writing it is a joy.

All JavaScript Promises live and die by the Promise() constructor:

const mypromise = new Promise(function(resolve, reject){
 // asynchronous code to run here
 // call resolve() to indicate task successfully completed
 // call reject() to indicate task has failed 
})

Using the resolve() and reject() methods internally, we can signal to a Promise object when a promise has been fulfilled and rejected, respectively. The then() and catch() methods can then be invoked to take care of the aftermath of a fullfilled or rejected promise.

I use a variation of the following Promise infused XMLHttpRequest function all the time to retrieve the contents of external files sequentially, one after the next:

function getasync(url) {
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest()
		xhr.open("GET", url)
		xhr.onload = () => resolve(xhr.responseText)
		xhr.onerror = () => reject(xhr.statusText)
		xhr.send()
	})
}

getasync('test.txt').then((msg) => {
	console.log(msg) // echos contents of text.txt
	return getasync('test2.txt')
}).then((msg) => {
	console.log(msg) // echos contents of text2.txt
	return getasync('test3.txt')
}).then((msg) => {
	console.log(msg) // echos contents of text3.txt
})

To master key aspects of JavaScript Promises, such as chaining Promises and executing Promises in parallel, read "Beginner's Guide to Promises".

3) JavaScript Async Functions

Along with JavaScript Promises, Async functions further rewrites the construct of traditional asynchronous code so it's more readable and easier to follow. Whenever I show clients code with async functions woven in, the first reaction is always astonishment, followed by a burning curiosity to understand how it all works.

An Async function consists of two parts:

1) A regular function prefixed with the async function

async function fetchdata(url){
	// Do something
	// Always returns a promise
	}

2) Use of the await keyword in front of asynchronous function calls within the main Async function.

A example is worth a thousand words. Here is a rewrite of the Promise based example above to use Async functions instead:

function getasync(url) { // same as original function
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest()
		xhr.open("GET", url)
		xhr.onload = () => resolve(xhr.responseText)
		xhr.onerror = () => reject(xhr.statusText)
		xhr.send()
	})
}

async function fetchdata(){ // main Async function
	var text1 = await getasync('test.txt')
	console.log(text1)
	var text2 = await getasync('test2.txt')
	console.log(text2)
	var text3 = await getasync('test3.txt')
	console.log(text3)
	return "Finished"
}

fetchdata().then((msg) =>{
	console.log(msg) // logs "finished"
})

The above example when run echos the contents of "test.txt", "test2.txt", "test3.txt", and finally "Finished" in order.

As you can see, inside the async function, we call the asynchronous function getasync() as if it was synchronous- no then() method or call back function needed to signal the next step. Whenever the await keyword is encountered, execution is paused until getasync() has resolved before moving on to the next line inside the Async function. The result is the same as a pure Promised based approach,  using a series of then() methods.

To master Async functions, including how to execute await functions in parallel, read "Introduction to JavaScript Async Functions- Promises simplified".

4) JavaScript Destructuring

Next to Arrow Functions, this is the ES6 feature I use the most on a daily basis. ES6 Destructuring isn't so much a new feature, but a new assignment syntax that lets you quickly unpack values from Object Properties and Arrays and assign them to individual variables.

var profile = {name:'George', age:39, hobby:'Tennis'}
var {name, hobby} = profile // destructure profile object
console.log(name) // "George"
console.log(hobby) // "Tennis"

Here I use destructuring to quickly extract the name and hobby properties of the profile object.

Using aliasing, you can use different variable names compared to the corresponding object properties' you're extracting values from:

var profile = {name:'George', age:39, hobby:'Tennis'}
var {name:n, hobby:h} = profile // destructure profile object
console.log(n) // "George"
console.log(h) // "Tennis"

Nested Objects Destructuring

Destructuring works with nested objects as well, which I use all the time to quickly unpack values from convoluted JSON requests:

var jsondata = {
	title: 'Top 5 JavaScript ES6 Features',
	Details: {
		date: {
			created: '2017/09/19',
			modified: '2017/09/20',
		},
		Category: 'JavaScript',
	},
	url: '/top-5-es6-features/'
};

var {title, Details: {date: {created, modified}}} = jsondata
console.log(title) // 'Top 5 JavaScript ES6 Features'
console.log(created) // '2017/09/19'
console.log(modified) // '2017/09/20'

Destructuring Arrays

Destructuring on arrays works similarly as on Objects, except instead of curly braces on the left hand side, use square brackets instead:

var soccerteam = ['George', 'Dennis', 'Sandy']
var [a, b] = soccerteam // destructure soccerteam array
console.log(a) // "George"
console.log(b) // "Dennis"

You can skip certain Array elements when unpacking its elements using a comma (,):

var soccerteam = ['George', 'Dennis', 'Sandy']
var [a,,b] = soccerteam // destructure soccerteam array
console.log(a) // "George"
console.log(b) // "Sandy"

For me, destructuring takes away all the friction of extracting and assigning of object properties and array values the traditional way. To fully grasp the intricacies and potential of ES6 destructuring, please read "Getting to Grips with ES6: Destructuring".

5) Default and Rest Parameters

And finally, the next 2 features of ES6 I want to bring up mostly deal with function parameters. Almost every function we create in JavaScript accepts user data, so these two features definitely come in handy more than once in a full moon.

Default Parameters

We've all used the below pattern when creating functions with parameters that should have default values:

function getarea(w,h){
var w = w || 10
var h = h || 15
return w * h
}

With ES6's support for default parameters, the days of testing for explicitly defined parameter values are over:

function getarea(w=10, h=15){
return w * h
}
getarea(5) // returns 75

More Details on Default Parameters in ES6 here.

Rest Parameters

Rest Parameters in ES6 makes it a cinch to turn function arguments into an array for easy manipulation.

function addit(...theNumbers){
	return theNumbers.reduce((prevnum, curnum) => prevnum + curnum, 0) // get the sum of the array elements
}

addit(1,2,3,4) // returns 10

By prefixing 3 dots (...) in front of a named argument, the parameters entered into the function at that position and onwards is automatically converted into an array.

Without Rest Parameters, we would have to do something convoluted like the below to manually transform the arguments into an array first:

function addit(theNumbers){
	var numArray = Array.prototype.slice.call(arguments) // force arguments object into array
	return numArray.reduce((prevnum, curnum) => prevnum + curnum, 0)
}

addit(1,2,3,4) // returns 10

Rest parameters can be applied to just a subset of a function's arguments, like the following, which only turns arguments starting from the 2nd into an array:

function f1(date, ...lucknumbers){
	return 'The Lucky Numbers for ' + date + ' are: ' + lucknumbers.join(', ')
}

alert( f1('2017/09/29', 3, 32, 43, 52) ) // alerts "The Lucky Numbers for 2017/09/29 are 3,32,43,52"

For the full specs on Rest Parameters in ES6, see here.

Conclusion

Do you agree with my top 5 ES6 features? Which ones do you use the most often? Please share in the comments below.