ES2015+: The parts I like

Author Jesse Breneman Published on April 9th, 2020

As a part of an ongoing learning series at work, I've been asked to give a presentation on new javascript features. This is a text version of that presentation! This is written assuming a familiarity with basic Javascript, and is mainly aimed at someone who knows javascript but maybe hasn't kept up with the latest features.

Some background on Javascript versions

Javascript is an implementation of the ECMAScript specification (abbreviated ES). Up to 2015, there had been 4 major editions and one abandoned edition. In 2015, the sixth edition (ES6) was published, which was later renamed to ES2015. From then on, the spec has been updated on a yearly cadence, with each edition being named after the year it was published.

ES2015

ES2015 (also known as ES6, although sometimes ES6 is used to refer to anything added to the spec after 2015. Yes, it's confusing!) adds a significant amount of new syntax to the spec. Further versions of the spec have a lot less changes, but because it had been a number of years since the last spec, ES2015 had a lot of new things.

let and const

ES2015 comes with two new ways of declaring variables, let and const, and you should be using these in place of var. Variables declared using const can't be reassigned, meaning once you assign it the reference has to stay as is. Using let to declare a variable will allow you to reassign, much like var. The other difference is how these new variables are scoped. var is function scoped, meaning it's available anywhere in the current function it's in, or if it's not in a function it's just in global scope. let and const are block scoped, meaning, they're scoped to the block they're in. Basically this means they're scoped to the current pair of curly brackets. :)

const foo = 'Test';

foo = 'Reassigning!'; // This throws a ReferenceError

let bar = 'Test';
bar = 'Reassigning!'; // All good!

One final thing to note is that const is not immutable, it only means that the variable can't be reassigned. Modifying an array or object that are declared with a const is perfectly valid.

const baz = {};

baz.message = 'Yes, this works.';

Generally, you should default to assigning variables using const, and if you need to reassign the variable, switch it over to a let. This gives the next person to read your code some clue into how you intend to use the variable.

Arrow functions

Arrow functions are a function shorthand that behave slightly differently than functions with regards to the this keyword.

function doSomething() {
    return 'I did it!';
}

const doSomethingElse = () => {
    return 'So did I!';
}
// ...Or
const doSomethingElse = () => 'So did I!';

// Particularly nice when used for a callback
[1, 2, 3, 4, 5].map(function (item) {
    return item * 2;
});

[1, 2, 3, 4, 5].map((item) => {
    return item * 2;
});
// ...Or
[1, 2, 3, 4, 5].map((item) => item * 2);

Arrow functions allow you to implicitly return, which basically means that you can omit the return keyword if your function is written in one line. That means that this function:

const multiline = () => {
    return 'Three lines? Ew!';
}

Can be written in one line:

const oneLine = () => 'No return needed here!';

Just keep in mind that if you're trying to return an object this way, you'll need to wrap it in parenthesis, otherwise the curly brackets get interpreted as a part of the function declaration.

// This will not work!
const nope = () => { one: 1 };

// Wrap in parenthesis!
const oneLine = () => ({ one: 1 });

The other nice thing about arrow functions is that unlike other functions, they do not change the meaning of this inside the function body. Ever had to reassign this to self to use it in a callback? No need to do that in an arrow function, this means the same thing inside an arrow function as it does outside it.

Template strings

A better way of constructing strings! Template strings can be multi-line, and allow you to interpolate variables inside them instead of concatenating strings together.

const multiline = `
    This is a
    multiline string!
`;

const firstName = 'Jesse';
const stringConcatenation = 'Hi there, ' + firstName + '!';
const interpolation = `Hi there, ${firstName}!`; // Much cleaner!

New string methods

Several new string methods have been added. The most useful ones are String.prototype.startsWith, String.prototype.endsWith, and String.prototype.includes;

const str = 'This is a test';

str.startsWith('This'); // true
str.includes('is a'); // true
str.endsWith('test'); // true

New array methods

Several new array methods have been added as well.

Array.prototype.from

Array.from converts an array-like object into an array. Useful for converting Sets and things like NodeLists into arrays. My preference is to use a spread operator (keep reading!) for this, but this also works.

const nodes = document.querySelectorAll('div'); // This is a NodeList, which is not an array!

const actualArray = Array.from(nodes); // This is now a proper array

Array.prototype.find and Array.prototype.findIndex

Array.find searches an array and returns the first value that it matches, using a callback that you define. Very useful when trying to find an object in an array of objects. Array.findIndex works the same way, but returns the index instead.

const arr = [orange, apple, grape];

const startsWithA = arr.find((item) => item.startsWith('a')); // Returns 'apple'

Promises

Promises are a new way of handling async code. Rather than using a callback, you create a promise (or use a language feature that creates one) and then chain actions on to it. Fetch, which is a replacement for XHR requests, returns a promise, so we're going to use that as an example.

fetch('https://ninjarockstar.dev/')
    .then((result) => {
        // Do stuff with the result
    })
    .catch((error) => {
        // The promise encountered an error, handle it here
    });

Promises can be created by initializing a new promise. When initializing a promise, you are given a resolve function that when run, will trigger the .then() chain, and a reject function that when run, will throw an error.

function timeout(duration = 0) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, duration);
    })
}

timeout(1000).then((result) => console.log('finished!'));

Object-literal improvements

Several shorthands were added to improve working with objects.

Initializing properties

Object properties can be directly initialized from variables now.

const name = 'Jesse';

// In ES5, you need to assign a variable to a key
const obj = {
    name: name
};

// In ES2015, you can remove that repetition. These examples are equivalent
const obj2 = {
    name
};

Initializing methods

Methods can be initialized using a new shorthand that omits the function keyword entirely.

const obj = {
    oldMethod: function () {

    },
    newMethod() {

    }
};

Computed properties

Property keys can be set dynamically using computed properties now, by wrapping an expression in brackets.

const key = 'name';

const obj = {
    [key]: 'Jesse'
};

Default arguments for functions

Function arguments now support defaults. Finally.

const request = (url, method = 'GET') => {

}

Rest/Spread

Rest and spread are new operators that give you the power to essentially collect and unpack arrays. They are pretty much two side of the same coin, so they share syntax: ....

Rest

Rest is used to collect the "rest" of something. This is typically useful for getting all of the arguments of a function, or when combined with destructuring (we'll get to that later).

const getArgs = (...args) => {
    return args;
}

Spread

Spread is essentially used to expand an array in place. This is useful for shallow copying and concatenating arrays.

const one = [1, 2, 3];
const two = [4, 5, 6];

const three = [...one, ...two]; // [1, 2, 3, 4, 5, 6]
const four = [...one]; // [1, 2, 3], but it's not a reference to one, it's a new array

Destructuring

Destructuring allows you to take a single variable like an array or object and transform it into multiple variables. Think of it as allowing you to essentially pull apart an array or object.

// This gives us three variables that are set to the three values in the array
// The position on the left side of the = determines the value
const [one, two, three] = [1, 2, 3];

// Combine with the rest operator to pull one or more values out and return the rest in an array
// This gives us two variables, one = 1 and rest = [2, 3]
const [one, ...rest] = [1, 2, 3];

This is particularly useful with objects since you have key names to reference.

const obj = {
    name: 'Jesse',
    favorites: {
        iceCream: 'Chocolate',
        superhero: 'Wolverine',
        drink: 'coffee'
    },
    hobbies: ['computers', 'cooking', 'photography']
};

// We can pull out the keys we're interested in
// Notice that you can pull out nested keys as well
const { name, favorites: { superhero } } = obj;

Modules

ES2015 introduces actual modules. There's a lot to this spec (and it keeps getting refined), but here's the basics.

Exporting

You can export using the export keyword in front of a variable or a function.

// math.js
export function sum(a, b) {
    return a + b;
}

export const pi = 3.141593;

And then import them in another file using import.

// app.js
import { sum, pi } from 'math.js';

console.log(`2π = ${sum(pi, pi)}`);

Additionally

I've covered what I consider to be the most useful features since there are a lot. Here's the ones I've skipped over.

  • Class syntax. It's a little sugar over prototypes if you don't like using them and are going for a more object oriented approach.
  • Iterators, for..of. Iterator objects give custom iteration. for..of is a nicer version of for..in.
  • Better unicode support.
  • Map, Set, WeakMap, WeakSet. More efficient data structures, Sets are similar to arrays and Maps are similar to objects.
  • Proxies. Allows you to set custom behaviors on top of objects.
  • Symbols. New primitive, primarily used to create unique property keys for objects.

ES2016

Whew. We made it. The rest of the sections will be smaller, I promise!

Array.prototype.includes()

No more indexOf()! This allows you to directly search an array for a match. Note that it is a straight comparison, so if you need to match a property in an object you'll need to use another method.

if ([1, 2].includes(2)) {
    console.log('Found!');
}

Exponentiation operator (**)

Math.pow(), but as an operator.

Math.pow(4, 2) === 4 ** 2;

ES2017

Object.values()

Returns all the values in an object as an array.

const obj = {
    name: 'Jesse',
    drink: 'coffee',
    hobbies: ['computers', 'cooking', 'photography']
};

Object.values(obj) // ['Jesse', 'coffee', ['computers', 'cooking', 'photography']]

Object.entries()

Returns an array containing all the object's properties as [key, value] pairs.

const obj = {
    name: 'Jesse',
    drink: 'coffee'
};

Object.values(obj) // [['name', 'Jesse'], ['drink', 'coffee']]

Async/await

Async functions are an easier, more readable way of using promises.

// Using promises
fetch('https://ninjarockstar.dev/')
    .then((result) => console.log(result))

// Using async/await
(async () => {
    const result = fetch('https://ninjarockstar.dev/');
    console.log(result);
})();

Notice that you do need to be in a function to use this syntax, as it requires you to tag the enclosing function as an async function. This example is a little simple, but you can already see how handy await can be since you can use the variable it returns just like any over variable, instead of being confined to a .then() callback.

Additionally

  • String padding. Allows you to add characters to the beginning or end of a string.
  • Trailing commas in functions.

ES2018

Rest/spread for objects

Rest/spread were updated to also work with objects.

// Rest & destructuring an object
const { first, second, ...others } = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5 };

// Spread allows you to recombine back into an object
const items = { first, second, ...others };

Additionally

  • Async iteration using await with for..of.
  • Promise.finally(). Allows you to run code when a promise is fulfilled, no matter if it's successful or not.
  • A number of regex improvements including lookbehinds, better unicode support, and named capture groups.

ES2019

Array.flat & Array.flatMap

Takes an array and flattens it to the depth you specify. flatMap is essentially a map function that also flattens to a depth of 1.

const numbers = [1, 2, [3, 4, [5, 6]]];

// Defaults to a single level
numbers.flat(); // [1, 2, 3, 4, [5, 6]]

// But you can set whatever depth you need
numbers.flat(2); // [1, 2, 3, 4, 5, 6]

// Amusingly enough, this is valid
numbers.flat(Infinity); //[1, 2, 3, 4, 5, 6]

Additionally

  • Object.fromEntries() is the reverse of Object.entries(), allowing you to convert back to an object easily.
  • String.trimStart and String.trimEnd. Pretty self explanatory.

ES2020

Nullish coalescing operator (??)

A slightly more type strict way of setting fallbacks. Normally you can use a double pipe to set a fallback for a variable, but often times there are values that are technically falsy but you don't want them to fall through, like 0 or an empty string. THe nullish coalescing operator will only fall through when the value is null or undefined.

const name = '';
const age = 0;

// Using ||, these will fall through to the default
console.log(name || 'Nobody'); // Nobody
console.log(age || 30); // 30

// Using ??, these will not fall through
console.log(name ?? 'Nobody'); // ''
console.log(age ?? 30); // 0

Optional chaining operator

Makes interacting with deeply nested objects significantly easier by allowing you to mark any part of the path as optional. You can use this by adding a question mark directly before the . in an object path.

const person = {};

// This will fail spectacularly
console.log(person.address.country);

// This will simply return undefined
console.log(person?.address?.country);

// This also works when accessing arrays and function calls
console.log(person?.hobbies?.[0]);
console.log(person?.getCountry?.());

Additionally

  • BigInt. If you need big numbers.
  • Dynamic imports, allowing you to use import as a function inline, using async/await.