The Coolest JavaScript Features from the Last 5 Years

ppiippaa

Pippa Thompson

Posted on February 27, 2023

The Coolest JavaScript Features from the Last 5 Years

According to Erik Qualman, language is always evolving. Although he was referring to natural language, the same applies to programming languages too. JavaScript has changed a lot since it’s conception in 1995, and since then many new features have been added. This article discusses some super useful (and possibly less well known) features added to JavaScript in the last 5 years. It in by no means an exhaustive list, and major revisions such as features surrounding classes will be discussed in a separate article.

A quick note on ECMAScript

ECMA (European Computer Manufacturers Association) is an organisation tasked with providing specifications and standards for programming languages, hardware and communications. ECMAScript is a part of this organisation which focuses specifically on scripting languages. In other words, it provides a “blueprint” for how a scripting language, such as JavaScript, should behave. JavaScript implements these specifications, and as ECMAScript evolves so too does JavaScript. In order for a new feature to be implemented there is a 4 step process presided over by the TC39 committee.

Due to the modernity of the features discussed in this article, some of them may not be supported by all browsers. To check browser compatibility, take a look at this link.

Now with all that out of the way, let's dive into the features.

String.padStart() and String.padEnd()

These string methods are a quick and easy way to attach strings onto other strings. As the names suggest, String.padStart() adds a new string onto the start of a given string and String.padEnd() appends a string onto the end of a given string. These methods do not mutate the original string.

String.padStart(desiredStringLength, stringToAdd)

  • desiredStringLength: How long you want the new string length to be as a number.
  • stringToAdd: this is the string you want to add to the beginning of the original string.

Let's take a look at an example:


let originalString = 'Script';

let paddedString = originalString.padStart(10, 'Java');

console.log(paddedString);

// OUTPUT -->
// 'JavaScript'

Enter fullscreen mode Exit fullscreen mode

What happens if the desiredStringLength argument is shorter than the length of the original string + the stringToAdd? In this case, the stringToAdd is truncated before it's added to the original string:


let originalString = 'Script';

let paddedString = originalString.padStart(7, 'Java');

console.log(paddedString);

// OUTPUT -->
// 'JScript'
// truncates the stringToAdd from 'Java' to 'J'

Enter fullscreen mode Exit fullscreen mode

What about it the desiredStringLength argument is longer than the length of the original string + stringToAdd? This can lead to some weird looking results! The stringToAdd argument will be repeated until it is equal to the desiredStringLength argument:


let originalString = 'Script';

let paddedString = originalString.padStart( 15, 'Java');

console.log(paddedString);

// OUTPUT -->
// 'JavaJavaJScript'

Enter fullscreen mode Exit fullscreen mode

And if no stringToAdd argument is provided? Empty spaces will be added to the front of the original string until the string length is equal to desiredStringLength:


let originalString = 'Script';

let paddedString = originalString.padStart(15);

console.log(paddedString);

// OUTPUT -->
// "         Script"

Enter fullscreen mode Exit fullscreen mode

And finally, what about it no desiredStringLength argument is provided? A copy of the original string is returned unchanged:


let originalString = 'Script';

let paddedString = originalString.padStart('Java');

console.log(paddedString);

// OUTPUT --> 
// 'Script'

Enter fullscreen mode Exit fullscreen mode

String.padEnd(desiredStringLength, stringToAppend)

This string method works in the same way as String.padStart(), but appends a string to the end of a given string.


let originalString = 'Web';

let paddedString = originalString.padEnd(6, 'Dev');

console.log(paddedString);

// OUTPUT -->
// 'WebDev

Enter fullscreen mode Exit fullscreen mode

The same rules apply regarding argument usage:

  • desiredStringLength < original string + stringToAppend? The stringToAppend appended to the end of the original string will be truncated.
  • desiredStringLength > original string + stringToAppend? The stringToAppend appended to the end of the original string will be repeated until the desiredStringLength is reached.
  • No stringToAppend argument passed? Empty spaces will be appended to the original string until the desiredStringLength is reached.
  • No desiredStringLength argument passed? A copy of the original string is returned unchanged.

String.replaceAll(pattern, replacement)

You may have come across String.replace() before, which takes a pattern argument and a replacement argument and replaces the first instance of the matching pattern in a string. The pattern argument can be a string literal or a RegEx.

String.replaceAll() takes it one step further and, as the name suggests, allows us to replace all instances of a specified pattern with a replacement string rather than just the first instance.


// Using String.replace() 
const aString = 'My name is Pippa. Pippa is my name.';

const replaceString = aString.replace('Pippa', 'Philippa');

console.log(replaceString);

// OUTPUT -->
// "My name is Philippa. Pippa is my name"
// only the first instance of 'Pippa' is replaced with 'Philippa'

// Using String.replaceAll() with regex
const  regex = /Pippa/ig;

const anotherString = 'My name is Pippa. Pippa is my name.';

const replaceAllString = anotherString.replaceAll(regex, 'Philippa');

console.log(replaceAllString);

// OUTPUT -->
// "My name is Philippa. Philippa is my name."
// both instances of 'Pippa' and replaced by 'Philippa'

Enter fullscreen mode Exit fullscreen mode

Object.entries(), Object.keys(), Object.values() and Object.fromEntries()

This set of methods are useful for transforming certain data structures. Let's go through them starting with...

Object.entries(originalObject)

This object method takes an object and returns a new 2-dimensional array with each nested array containing the original object’s key and value as elements. Let me show you what I mean:


const fruitObject = {
  'banana': 'yellow',
  'strawberry': 'red',
  'tangerine': 'orange' 
};

const fruitArray = Object.entries(fruitObject);

console.log(fruitArray);

// OUTPUT -->
// [["banana", "yellow"], ["strawberry", "red"], ["tangerine", "orange"]]

Enter fullscreen mode Exit fullscreen mode

This can be a super helpful method to use when transforming data. Another use case would be to access a specific key-value pair in an object:


const fruitObject = {
  'banana': 'yellow',
  'strawberry': 'red',
  'tangerine': 'orange' 
};

const firstFruit = Object.entries(fruitObject)[0];

console.log(firstFruit);

// OUTPUT -->
// ['banana', 'yellow']

Enter fullscreen mode Exit fullscreen mode

In case you hadn’t heard, a lot of things in JavaScript are objects. So, we can even pass arrays and strings as arguments into the Object.entries() method which will coerce them into objects. Let’s see what happens when we pass a string as an argument:


const string = 'Hello'

const stringAsArgument = Object.entries(string);

console.log(stringAsArgument);

// OUTPUT --> 
// [["0", "H"], ["1", "e"], ["2", "l"], ["3", "l"], ["4", "o"]]

Enter fullscreen mode Exit fullscreen mode

Each character in the string is inserted into a separate array, and its index is set as the first element of the array. This behaviour also happens when you pass an array as an argument:


const array = [1,2,3]

const formattedArray = Object.entries(array);

console.log(formattedArray);

// OUTPUT --> 
// [["0", 1], ["1", 2], ["2", 3]]

Enter fullscreen mode Exit fullscreen mode

Note that for both of these cases the first element (the index) is a string.

Object.keys(anObject)

This object method accepts an object as an argument and returns an array containing the object’s keys as elements.


const programmingLangs = {
  'JavaScript': 'Brendan Eich', 
  'C': 'Dennis Ritchie',
  'Python': 'Guido van Rossum'
};

const langs = Object.keys(programmingLangs);

console.log(langs);

// OUTPUT -->
// ["JavaScript", "C", "Python"]

Enter fullscreen mode Exit fullscreen mode

What about if we try and pass a string as the argument? Let’s take a look:


const string = 'Hallo';

const stringArray = Object.keys(string);

console.log(stringArray);

// OUTPUT -->
// ["0", "1", "2", "3", "4"]

Enter fullscreen mode Exit fullscreen mode

In this case the string is also coerced into an object. Each letter represents the value and its index represents the key, so we are left with an array containing the indices of each letter in the string.

Object.values(anObject)

As you might expect, the Object.values() method works similarly to the method we just discussed, but instead of returning out an object’s keys in an array it returns out an object’s values in an array. Let’s use the programmingLangs example we saw previously:


const programmingLangs = {
  'JavaScript': 'Brendan Eich', 
  'C': 'Dennis Ritchie',
  'Python': 'Guido van Rossum'
};

const creators = Object.values(programmingLangs);

console.log(creators);

// OUTPUT -->
// ["Brendan Eich", "Dennis Ritchie", "Guido van Rossum"]

Enter fullscreen mode Exit fullscreen mode

As we saw in the previous case of Object.entries() and Object.keys(), we can pass in other data types such as a string.


const string = 'Bonjour'

const stringArray = Object.values(string);

console.log(stringArray) 

// OUTPUT -->
// ["B", "o", "n", "j", "o", "u", "r"]

Enter fullscreen mode Exit fullscreen mode

Object.fromEntries(anIterable)

Another super useful method for transforming data. You remember the Object.entries() method we saw earlier that turns an object into a 2-dimensional array? Well, Object.fromEntries() essentially does the opposite. It accepts an iterable as an argument, such as an array or a map, and returns an object. Let’s take a look:


const arrayTranslations = [
   ['french', 'bonjour'], 
   ['spanish', 'buenos dias'], 
   ['czech', 'dobry den']
];

const objectTranslations = Object.fromEntries(arrayTranslations);

console.log(objectTranslations);

// OUTPUT --> 
/* [object Object] {
  czech: "dobry den",
  french: "bonjour",
  spanish: "buenos dias"
} */

Enter fullscreen mode Exit fullscreen mode

So our iterable, in this case the nested array stored as translations, is iterated through and each subarray transformed into an object with element at index 0 as the key and element at index 1 as the value. Handy!

Array.flat(optionalDepthArgument)

Useful when it comes to dealing with multi-dimensional arrays, the array method .flat() takes a given array and returns a flat array (1-dimensional by default) or an array of a specified depth (when the optionalDepthArgument is provided).

When no optionalDepthArgument is provided, the default depth is 1:


const numArray = [1, 2, [3,4]];

const flatArray = numArray.flat();

console.log(flatArray);

// OUTPUT -->
// [1, 2, 3, 4]

Enter fullscreen mode Exit fullscreen mode

The following is an example where an optionalDepthArgument has been passed into the method:


const numArray = [1, 2, [[[3,4]]]];

const depthOf2Array = numArray.flat(2);

console.log(depthOf2Array);

// OUTPUT -->
// [1, 2, [3, 4]];

Enter fullscreen mode Exit fullscreen mode

In the above snippet, we specified an optionalDepthArgument of 2 so our new array depthOf2Array is array with 2 ‘levels’. If you have a heavily nested array and want to return a one-dimensional array but you aren’t sure of the depth of the array, you can pass in the argument Infinity like so:


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

const oneDimensionalArray = nestedArray.flat(Infinity);

console.log(oneDimensionalArray); 

// OUTPUT -->
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

Enter fullscreen mode Exit fullscreen mode

Object Spread Operator ...

You may have seen the spread syntax (…) in JavaScript before, however up until recently we could only use it with arrays. It’s a great method for cloning and merging arrays. As of ES2018, we can now harness this power to use with objects. The use cases of the object spread operator are the same as with the array spread operator; cloning and merging.

Let’s first take a look at how we’d use this operator to clone an object and add extra key-value pairs:


const bookAndAuthor = {
  'Gabriel Garcia Marquez': '100 Years of Solitude'
}

const moreBooksAndAuthors = {
  ...bookAndAuthor, 
  'Paolo Coelho' : 'The Alchemist'
}; 

console.log(moreBooksAndAuthors)

// OUTPUT -->
/* [object Object] {
  Gabriel Garcia Marquez: "100 Years of Solitude",
  Paolo Coelho: "The Alchemist"
}
*/

Enter fullscreen mode Exit fullscreen mode

Here we’ve been able to very easily clone the original object and add more properties.

Importantly, using the object spread operator to copy an object with no nested data will create a new object in memory. This means that we can clone an object, change or add properties to the newly created object, but the original object will not be mutated. However, if we clone an object that contains nested data, the clone will be stored in a new place in memory, but the nested data will be passed by reference. This means that if we were to make changes to any nested data in one object, the same nested data in the second object would also change.

Now, let's see what happens when we use the object spread operator to merge objects:


const book1 = {
  'Milan Kundera': 'The Unbearable Lightness of Being'
}

const book2 = {
  'Bohumil Hrabal': 'I Served the King of England'
}

const books = {...book1, ...book2 }

console.log(books);

// OUTPUT --> 
/*
  [object Object] {
  Bohumil Hrabal: "I Served the King of England",
  Milan Kundera: "The Unbearable Lightness of Being"
}
*/

Enter fullscreen mode Exit fullscreen mode

Here we have merged two object to create a new object. How easy was that?

Promise.finally() and Promise.allSettled()

Promise.finally(callbackFunction)

Promises in JavaScript are nothing new and have been around since ES6, but the Promise.finally() method is a newer addition to the async JavaScript toolbox. This method takes a callback function which is executed once the promise is settled, i.e. resolved or rejected. It is a good place to run code that relates to any clean up tasks.


const promise = new Promise((resolve, reject) => {
  let num = Math.floor(Math.random());

  if (num >= 0.5) {
    resolve('promise resolved')
  } else {
    reject('promise rejected')
  }
})

promise
  .then(value => console.log(value))
  .catch(error => console.log(error))
  .finally(() => console.log('promise has been settled'));

// OUTPUT -->
// 'promise resolved' / 'promise rejected' (depending on value of num)
// 'promise has been settled'

Enter fullscreen mode Exit fullscreen mode

Regardless of whether the promise is resolved or rejected, the callback function in the Promise.finally() method will always be executed.

Promise.allSettled([promises])

An ES2020 addition to JavaScript, the Promise.allSettled() method accepts an array of promises and returns a new promise which only resolves once all promises in the array have been settled (resolved or rejected). Once resolved, the return value will be an array of objects, with each object describing the outcomes of the promises passed in the array.


const promise1 = new Promise((resolve, reject) => {
  resolve('I have been resolved')
}); 

const promise2 = new Promise((resolve, reject) => {
  reject('I have been rejected')
});

Promise.allSettled([promise1, promise2])
  .then(result => console.log(result))

// OUTPUT --> 
/*
[[object Object] {
  status: "fulfilled",
  value: "I have been resolved"
}, [object Object] {
  reason: "I have been rejected",
  status: "rejected"
}]
*/

Enter fullscreen mode Exit fullscreen mode

In this example, on line 9 we declare Promise.allSettled() and we pass this method an array containing promise1 and promise2. On line 10, we chain a .then() method to Promise.allSettled() which instructs JavaScript to print out the resolved value of Promise.allSettled(). The output shows an array of objects has been returned. Each object represents the outcome of the promises passed as arguments to Promise.resolve() (in our case, promise1 and promise2). These objects have 2 properties; status, which evaluates to either fulfilled or rejected, and value, which evaluates to either rejected if the promise has been rejected, or the resolved value if the promise has been resolved.

BigInt

This handy data type is used to store integer values in a variable which are too large to be stored as a Number data type. You may or may not know that the JavaScript Number data type has limits on the size of integers it can store - the safe range is from -9007199254740991 -(253-1) up to 9007199254740991 +(253-1), so 15 digits. The introduction of this data type (which has a typeof value of “bigint”) allows us to work more easily with integers outside this range.

A few notes about using BigInt:

  • Arithmetic operators which work with the Number data type in JavaScript can also be used with BigInt, such as +, *, / etc.
  • BigInt cannot be used with decimals.
  • You cannot perform arithmetic operations between a BigInt data type and a Number data type.

There are 2 ways to declare a variable as a BigInt data type, using BigInt(number) or appending n to a number:


let hugeNumber = BigInt(9999999999999999);
let anotherHugeNumber = 7777777777777777n;

console.log(typeof hugeNumber);
console.log(typeof anotherHugeNumber);

// OUTPUT -->
// 'bigint'
// 'bigint'

Enter fullscreen mode Exit fullscreen mode

Nullish Coalescing Operator and Optional Chaining

Arguably my most used features from this list.

Nullish Coalescing (??)

This logical operator takes two operands. If the left-hand side operand is null or undefined it will return the right-hand side operand. On the flip side, if the left-hand side operand is not null or undefined, it will return the left-hand side operand.

It’s similar to the logical Or operator (||), except the || operator returns the right-hand side based on whether the left-hand side is a falsy value (as opposed to just null or undefined). There will be some behavioural overlap between ?? and || as falsy values in JavaScript include null and undefined, but also 0, “” (empty string), Nan and, of course, false.

Let's see this in action:


const usingOr = undefined || 'Return me because undefined is a falsy value';

const usingOrAgain = 'Return me because I am NOT falsy' || 'I will not be returned'


const usingNullishCoalescing = undefined ?? 'Return me!';

const usingNullishCoalescingAgain = 'I will return because I am NOT null/undefined ' ?? 'I will not be returned';


console.log(usingOr)
console.log(usingOrAgain)
console.log(usingNullishCoalescing)
console.log(usingNullishCoalescingAgain)

// OUTPUT -->
// "Return me because undefined is a falsy value"
// "Return me because I am NOT falsy"
// "Return me!"
// "I will return because I am NOT null/undefined " 

Enter fullscreen mode Exit fullscreen mode

In terms of operator precedence, the Nullish Coalescing operator has the 5th lowest precedence, so bear this in mind when combing multiple operators. For more in-depth information about operator precedence you can check out this page.

Optional Chaining (?.)

This operator is used when accessing properties or methods of an object. It helps us avoid throwing errors if a property/method does not exist. Rather than an error, we receive undefined if no corresponding property/method is found.

  • For use with object properties we access the property with Object?.property rather than just Object.property.
  • For use with object methods we invoke the method with Object.method?.() rather than just Object.method().

const person = {
  name: 'Pippa',
  favouriteColour: 'green',

  sayHello() {
    return `${this.name} says hello`;
  }
}

// ?. with object properties 
const color = person?.favouriteColour;
const age = person?.age;

console.log(color);
console.log(age);

// ?. with object methods
console.log(person.sayHello?.());
console.log(person.sayGoodby?.());

// OUTPUT --> 
// "green"
// undefined
// "Pippa says hello"
// undefined

Enter fullscreen mode Exit fullscreen mode

As you can see, both person.age and person.sayGoodbye() do not exist on the person object, but instead of receiving an error we get undefined returned to us.

Numeric Separator(_)

Let’s finish this list with an easy one. Numeric separators were introduced into JavaScript for readability when working with larger numbers. They allow you to ‘break down’ numbers into more easily digestible chunks, exactly like you would when using a comma (,) or point (.). We can separate larger numbers using the _ character (underscore).


const harderToReadNumber = 100000000
const easierToReadNumber = 100_000_000

// how much nicer is that to read?!

Enter fullscreen mode Exit fullscreen mode

If you made it this far, thanks for reading! I hope that you’ve learnt something which will serve you well in future JavaScript projects.

💖 💪 🙅 🚩
ppiippaa
Pippa Thompson

Posted on February 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related