JavaScript in 2018

JavaScript, both as a language and as an ecosystem, is probably the most evolving development environment there is. Originally invented for the browser, nowadays JS is used everywhere from server-side components to mobile and game development. In 2018 JavaScript can be written in a way that does not resemble language as we knew it five or ten years ago. In this post I'm going to show a simple example of modern JS that uses many features that have not been available just a few years (or in some cases - months) ago.

Let's consider a common scenario where we want a function that takes some arguments, performs a request to a remote API and returns some data, here is how this code could look like in 2018:

const getData = async (endpoint, { format = "json", headers = {}, ...opts} = {}) => {
	headers = { 'Accept': format === 'json' ? 'application/json' : '*/*', ...headers };
	const response = await fetch(`https://api.example.com/${endpoint}`, {...opts, headers });
	return await format === 'json' ? response.json() : response.text();
}

Short and sweet, does a lot in just a few lines and works in modern browsers and node! Unfortunately for few more years we will need to use transpilation and polyfills in production so that we can handle the few not-so-clever browsers out there. Oh and node doesn't really have fetch, but it can be npm-installed!

Let's talk about new JavaScript features used in the above snippet one by one.

Destructuring assignment

Destructuring assignment syntax makes it possible to unpack properties from objects into individual variables, e.g.

const { scrollX, scrollY } = window;
console.log(scrollY); // outputs current scroll position

BTW: it also works with arrays:

const [foo, bar] = ['lorem', 'ipsum'];
console.log(foo); // outputs "lorem"

Destructuring can also be used in parameter handling, where it's possible to provide default values, pretty much enabling named parameters in JS, e.g.

function baz({ foo = 'foo', bar = 'bar' }) {
	console.log(foo, bar);
}
baz({ bar: 'lorem ipsum' }); // outputs "foo" "lorem ipsum"

In the original getData snippet, second argument to getData function is being destructured into three distinct variables: format, headers and opts (for that last one, the rest syntax is used, see spread operator section below).

ES6 parameter handling

ES6 introduces many enhancements to parameters handling. Among other things it enables destructuring described above in parameter handling. It also enables default values, e.g.

function baz(foo, bar = 42) {
	console.log(foo, bar);
}
baz('lorem ipsum'); // outputs "lorem ipsum" 42

In the original getData snippet, second parameter uses destructuring, provides default values for extracted variables and has a default value of an empty object.

Spread operator

With spread operator it's possible to expand iterables and strings into array literals and function parameters e.g.

[..."foo"].reverse(); // outputs ["o", "o", "f"]
const numbers = [3, 1, 7, 9, 4];
Math.max(...numbers); // outputs 9

It's also possible to expand objects, optionally overriding existing properties e.g.

const opts = { 'foo': 42, bar: 'bazinga!' };
console.log({
	bar: 'baz',
	...opts
}); // outputs {bar: "bazinga!", foo: 42}

Spread operator (...) is also used for collecting rest elements/properties/arguments, e.g.

const [foo, bar, ...baz] = [1, 2, 3, 4, 5]; //foo = 1, bar = 2, baz = [3, 4, 5]
const { foo, ...rest } = { foo: 42, bar: 'lorem', baz: 'ipsum' }; // foo = 42, rest = { bar: 'lorem', baz: 'ipsum' }

In the original getData snippet, spread operator is used multiple times. First any optional arguments that are not used directly are collected into ...opts. Later Accept header is derived from format optional argument and is placed in headers together with any header that might have been passed over as an optional argument. However if headers already contains Accept, that will be used instead. Finally spread operator is used when building second argument for fetch, where the original opts are expanded, but headers property is overridden so that amended headers are used.

Promises and async/await

Promises represent an asynchronous operation and enable attaching handlers for both successful and failed results of such operation. Historically asynchronous functions would use a callback (or a pair of callbacks) that are expected to handle the outcome of the operation. A good example is node's file system operations that take a single callback to handle both an error and success outcome, In general working with promises is much more pleasant than working with callbacks. Promises can be returned from functions, sent as parameters, have multiple handlers attached etc.

Here is a dummy example that randomly resolves or rejects a promise after two seconds:

function getPromise() {
	return new Promise((resolve, reject) => setTimeout(() => { 
		Math.random() > 0.5 ? resolve(42) : reject(0)
	}, 2000) );
};

Which can be used like this:

getPromise()
	.catch(console.error) // in 50% cases after two seconds errors 0 to console
	.then(console.log) // in 50% cases after two seconds outputs 42 to console

Promises are part of ES6, however there is already syntax that builts on top of promises in more recent ES2017, namely async/await. With this syntax it's easy to avoid callback hell when dealing with sequential promises. Firstly function that is async always returns a Promise, e.g.

async function baz() { return 42; };
const promise = baz(); // a resolved promise
promise.then(console.log) // outputs 42

An important aspect of an async function is that it's possible to "wait" for another Promise (or async function) to resolve within body of such function using await keyword. Should that promise result in failure, we can catch it using standard try/catch syntax. Here is example using dummy getPromise function from Promises example above:

function getPromise() {
	return new Promise((resolve, reject) => setTimeout(() => { 
		Math.random() > 0.5 ? resolve(42) : reject(0)
	}, 2000) );
};

async function baz() {
	try {
		const answer = await getPromise();
		console.log(answer);
	} catch(e) { console.error(e); }
};

Going back to the original getData snippet, the entire function is marked async, meaning it will return a promise. It also uses await twice: first waiting for fetch to complete a network call and resolve with a Response object. Second to parse the response object, either as text or json.

Should fetch operation fail, error will be thrown that should handled by the end-user of getData function.

Fetch

Fetch is a modern way of making network requests in the browser. It's much nicer to work with than XMLHttpRequest and with a polyfill can be used universally in node and browser environments.

Arrow functions

Arrow functions are not only a short version of function expressions but also do not have own this, meaning no more dreadful var that = this. Arrow functions are useful everywhere, but are especially nice for simple callbacks, e.g.

[1, 2, 3, 4, 5].filter(n => n % 2); // [1, 3, 5]

The GetData snippet above could as well just be a normal function, arrow function is just a nicer, shorter syntax.

Template literals

Template literals are string literals that allow for embedded expressions. Main advantage of template literals is readability, e.g.

const phrase = "Use the force";
const [ep4, ep5, ep6] = [2, 1, 0];
// bit easier on eyes
console.log(`Phrase "${phrase}" is used ${ep4 + ep5 + ep6} times in total in the original Star Wars trilogy`); 
// bit hard to read/write
console.log('Phrase "' + phrase + '" is used ' + (ep4 + ep5 + ep6) + ' times in total in the original Star Wars trilogy');

The GetData snippet above could as well just use normal concatenation but template literals are more pleasant to read/write.

Block-scoped declarations

ES 6 introduces two new declarations: let and const. These are block-scoped as opposed to var which is function-scoped. Another difference from var is that values assigned using let or const cannot be used before the assignment happens. Finally const cannot be reassigned.

The GetData snippet above, as well as all the examples I've used const everywhere. This is my personal preference, I believe using const hints that this variable won't be reassigned and is scoped for this block. It real-world const is my default go to, unless I expect re-assginment and/or need different scope in which case I will use let or var as needed.

Summary

With just a few lines of almost-real-world code I was able to use eight different features from various specs (ES6 aka ES2015, ES2017, ES draft aka ES2019, Fetch Standard) that did not exist in JavaScript just a few years ago. I'm definitely glad for this evolution because modern JavaScript is much more pleasant to work with. That being said, keeping up to date with latest development in JavaScript its ecosystem feels almost like a full-time job on its own.