The Coolest JavaScript Features from the Last 5 Years
Tags: webdev, javascript, beginners, codenewbie
Published: 2023-02-27 21:28:31

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](https://caniuse.com/).

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:

```javascript

let originalString = 'Script';

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

console.log(paddedString);

// OUTPUT -->
// 'JavaScript'

```

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: 

```javascript

let originalString = 'Script';

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

console.log(paddedString);

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

```
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:

```javascript

let originalString = 'Script';

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

console.log(paddedString);

// OUTPUT -->
// 'JavaJavaJScript'

```

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:  

```javascript

let originalString = 'Script';

let paddedString = originalString.padStart(15);

console.log(paddedString);

// OUTPUT -->
// "         Script"

```

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

```javascript

let originalString = 'Script';

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

console.log(paddedString);

// OUTPUT --> 
// 'Script'

```


#### 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. 

```javascript

let originalString = 'Web';

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

console.log(paddedString);

// OUTPUT -->
// 'WebDev

```

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. 

```javascript

// 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'

```


### 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:

```javascript

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

const fruitArray = Object.entries(fruitObject);

console.log(fruitArray);

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

```

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:

```javascript

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

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

console.log(firstFruit);

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

```

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:

```javascript

const string = 'Hello'

const stringAsArgument = Object.entries(string);

console.log(stringAsArgument);

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

```

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:

```javascript

const array = [1,2,3]

const formattedArray = Object.entries(array);

console.log(formattedArray);

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

```
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. 

```javascript

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

const langs = Object.keys(programmingLangs);

console.log(langs);

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

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

```javascript

const string = 'Hallo';

const stringArray = Object.keys(string);

console.log(stringArray);

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

```
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:

```javascript

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"]

```

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

```javascript

const string = 'Bonjour'

const stringArray = Object.values(string);

console.log(stringArray) 

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

```

#### 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:

```javascript

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"
} */

```
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:

```javascript

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

const flatArray = numArray.flat();

console.log(flatArray);

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

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

```javascript

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

const depthOf2Array = numArray.flat(2);

console.log(depthOf2Array);

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

```

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:

```javascript

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]

```

### 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:

```javascript

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"
}
*/

```
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:

```javascript

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"
}
*/

```
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.

```javascript

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'

```
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. 

```javascript

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"
}]
*/

```
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:

```javascript

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

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

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

```

### 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:

```javascript 

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 " 

```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence). 

#### 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()`. 

```javascript

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

```
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).

```javascript

const harderToReadNumber = 100000000
const easierToReadNumber = 100_000_000

// how much nicer is that to read?!

```

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.