JavaScript: Post-ES6 Additions

ES6 (2015) was a major release of the language, with multiple new features introduced. It marked a turning point in modern web development. ES6 is still synonymous for “modern JavaScript” in the development community.

There have been multiple releases of the langauage since then though, and each brought new features, to enrich and optimize the language.

Here are some highlights of what was introduced in the post-ES6 years.

Array.prototype.includes

The includes method was added to the String object in ES6. Subsequently, it was added to the Array object as well. Its naming is self explanatory – it checks whether a given value exists as a member of an array, and returns a boolean – true or false.

E.g.:

const myArray = [1,2,3,4,5,6,7];
myArray.includes(7); //expected output true
myArray.includes(0); //expected output false

The above is a very convenient shortcut. Previously, to find out whether the array contains a certain value one would have to loop through the whole array and compare each member to the requested value. E.g.:

const arrayIncludes = (arr, x) => {
    let returnValue = false;
    arr.forEach((el) => {
        if(el === x) {
            returnValue = true;
        } 
    }); 
    return returnValue;
}

arrayIncludes(myArray, 7);
arrayIncludes(myArray, 0);

Object.values, Object.entries

ES5 introduced the Object.keys method, which returns an array of an object’s keys.

In the same fashion, Object.values returns an array of the values of the objects key-value pairs.

While Object.entries returns an array of an object’s key-value pairs.

E.g.:

let obj1 = {a:1, b:2, c:3}
Object.keys(obj1); //expected output [a,b,c] Object.values(obj1); //expected output [1, 2, 3] Object.entries(obj1); //expected output [['a', 1],['b', 2],['c', 3]]

Q1: How did we retrieve the values of an object before Object.values?

Q2: How did we retrieve the values of an object before Object.keys?

One potential use of Object.entries is where we might want to convert an object to a map. E.g.:

const map1 = new Map(Object.entries(obj1));

Async/await

The async function declaration creates a binding of a new async function to a given name. The await keyword can be used within the function body, enabling asynchronous, promise-based behavior to be written in a cleaner style and avoiding the need to explicitly configure promise chains.

We associate async/await with ES6, but this functionality was actually added in ES7 (2017). ES6 introduced promises – we’ll make those the topic of a separate discussion.

E.g.:

function returnIn3Seconds() {
  return setTimeout(() => {
      console.log(`All the world\'s a stage, and all the men and women merely players.`);
    }, 3000);
}

async function asyncCall() {
  console.log('calling');
  const result = await returnIn3Seconds();
  console.log(result);
}

asyncCall();

Was it possible to do asynchronous programming before introducing the async functions? It was, through using callback functions.

function returnIn3Seconds() {
  return setTimeout(() => {
      console.log(`All the world\'s a stage, and all the men and women merely players.`);
    }, 3000);
}

function asyncCall(callback) {
  console.log('calling');
  const result = callback();
  console.log(result);
}

asyncCall(returnIn3Seconds);

In simple cases that was just as straightforward, but when one wanted to call data from different sources and manipulate that data before returning a result, one had to deal with multiple callbacks, which made the code hard to read and manage, and was error-prone. That’s an antipattern popularly known as “callback hell”.

Exponentiation Operator (**)

The ** operator is a convenient way to raise a number to the power of another number. E.g.:

3**3 //expect 27

Previously we had to use a pow method of the Math object. E.g.:

Math.pow(3,3) //expect 27

Spread operator (…) for Objects

ES6 added the spread syntax to Arrays, while in ES9 (2018) it was also enabled for objects. E.g.:

let obj1 = {a:1, b:2, c:3, d:4}; 

let obj2 = {...obj1, e:5}; //expected  {a:1, b:2, c:3, d:4, e:5}  

Why would we want to create a new object in the way illustrated above? Can’t we just write

let obj2 = obj1;
obj2.e = 5;

Although logical, that will not yield the desired result. In JavaScript, creating an object by assignment actually creates a pointer to the same object in memory, not a new object. So any change to obj2 above will also change obj1. If you run the above code, they you get:

console.log(obj2) //expected {a: 1, b: 2, c: 3, d: 4, e: 5}
console.log(obj1) //unexpected, but {a: 1, b: 2, c: 3, d: 4, e: 5}

Was it possible to create a new object from an existing one before the spread syntax? Yes, but it was more verbose:

let obj3 = Object.assign({}, obj2);
obj3.f = 6;
console.log(obj3) //expect {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}, while obj2 and obj1 remain unchanged. 

And the assign method was actually only introduced with ES6. Before that developers had to resort to libraries, or more complicated manual trickery.

There still remains the question of shallow vs deep copies of an object, but that can be the topic of a separate discussion. Suffice it to say that for the purpose of deep object copies, ES10 (2019) introduced the global function structuredClone().