Introduction

Hated by many, and also loved by many, JavaScript is one of the de-facto web development standards. Up to recently, it had a monopoly when it comes to developing web-based apps (this is no longer the case because of the WebAssembly or WASM), and it has gone a long way since its beginnings. In this article, I will cover 8 new features introduced as a part of ES10 or if you prefer, ES2019.

This is a list of (8) new ES features we will be covering here:

  • Optional Catch Binding
  • Object.fromEntries
  • Array.flat
  • Array.flatMap
  • String#{trimStart,trimEnd}
  • Dynamic Import (at the time of writing this article, the proposal was in stage 3 - see here)
  • globalThis object (at the time of writing this article, the proposal was in stage 4 - see here)

Do note that there are a couple more of them introduced in ES10. This article covers only the ones I find most usable in practice. Feel free to comment if you have anything to add

1. Optional Catch Binding

Until this was possible, we as developers were forced to bind an exception whenever we used try...catch statement. The engine didn't care if the exception was later used or not, which was a bad thing.

Bad:

try {
  // some code
  return true;
} catch(unusedException) { // here is the problem
  return false;
}

Good (the new ES10 feature):

try {
  // some code
  return true;
} catch {
  return false;
}

As you can see, we are now able to omit the exception binding if it's not required. If you ask me, it's an improvement. This way our code is cleaner and less confusing for the next guy (a person that will continue our work at some point in the future).

2. Object.fromEntries()

It might not be that clear from the method name, but the Object.fromEntries method transforms a list of key-value pairs into an object. You can transform a map or array of arrays into an object. It does the reverse of Object.entries, which converts the given object into its key-value pair representation.

Imagine that you have a list of users containing the user name and age as individual items. You can use Object.fromEntries to transform that list into an object.

const users = [['John', 49], ['Frank', 25], ['David', 36]];
const usersAge = Object.fromEntries(users);
console.log(usersAge);

// outputs: {John: 49, Frank: 25, David: 36}

3. Array.flat()

This method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth, meaning that we can get a single array as a result in the case when we have an array of arrays. Not every item of the source array needs to be an array. Array.flat is smart enough to figure that out. The default depth is 1.

We can try to illustrate our example by thinking of the link between postal codes and cities. Sometimes, the same postal code can reference two cities if they are located in a different state (if I'm not mistaken :)). And your application might require a list of all cities regardless of its postal code, therefore it might end up with a list looking something like this:

['City 1', ['City 2'], ['City 3', 'City 4']]

To make this list easier to use and iterate through, we could flatten it by using the Array.flat method.

const cities = ['City 1', ['City 2'], ['City 3', 'City 4']];
console.log(cities.flat()); // outputs ["City 1", "City 2", "City 3", "City 4"]

Also, I find it useful to mention that Array.flat will remove all empty slots from our array.

const numbers = [1, 2, [3, 4],, 5, 6,, [7,8]];
console.log(numbers.flat()); // outputs [1, 2, 3, 4, 5, 6, 7, 8]

4. Array.flatMap()

This method first maps each element using a mapping function, then flattens the result into a new array.

For this example, we could modify the users list from the Object.fromEntries example above.

Imagine that, besides name and age, we receive the user followers within each item in the list. Object.fromEntries cannot help us in this case because it will simply ignore the third element considering that it only works with key-value pairs meaning that the maximum number of items for each element is 2.

Instead, we should use Array.flatMap to achieve the same-ish result. In my opinion, this result is a little better because it will provide more context.

const users = [['John', 49, 96], ['Frank', 25, 388], ['David', 36, 14]];
const usersFlattened = users.flatMap(([name, age, followers]) => {
    return { name, age, followers };
});
console.log(usersFlattened); 

// outputs:
// 0: {name: "John", age: 49, followers: 96}
// 1: {name: "Frank", age: 25, followers: 388}
// 2: {name: "David", age: 36, followers: 14}
//   length: 3

5-6. String.trimStart() & String.trimEnd()

String.trimStart method removes whitespace from the beginning of a string, and String.trimEnd method removes whitespace from the end of a string. Both of them have an alias, trimLeft and trimRight correspondingly.

const message = '   Hello ES10   ';

console.log(message.trimStart()); // outputs 'Hello ES10   '
console.log(message.trimEnd()); // outputs '   Hello ES10'
console.log(message); // outputs '   Hello ES10   '

Note that both of these methods return a new string with whitespaces stripped. The original string is not modified.

7. Dynamic Import

By dynamically importing a module we are receiving a promise for the module namespace object (exports) of the requested module. We can go one step further and also use async/await to assign the import to a variable.

Our module could look something like this:

// Default export
export default () => {
  console.log('Do default');
};

// Named export
export const doSomething = () => {
  console.log('Do something');
};

We are now able to import it dynamically in one of the following ways:

import('..my-path/my-module.js').then((module) => {
  module.default();
  module.doSomething();
  // ...
});

or

(async () => {
  const module = await import('..my-path/my-module.js')
  module.default();
  module.doSomething();
  // ...
})();

8. globalThis Object

Prior to the globalThis standardization, we had a couple of ways to determine the global object of the environment our application resides in. The first approach was to use the Function like this:

const global = Function('return this')();

For example, in browser, this will return the Window object. However, the given approach causes CSP violations which denies the Function usage in such a manner and due to that we had to use a manual check to determine the global object, like this:

const getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
}

const global = getGlobal(); // will return window object in the browser

// array usage example
const numbers = new global.Array(1, 2, 3);
console.log(numbers); // outputs [1, 2, 3];

Usually, library builders use this approach and wrap their code with the immediate function providing the correct global object for the library. It looks something like this:

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
            (global = global || self, global.myLib = factory());
}(this, function () {
    // the code

    return {} // return the lib namespace
}));

In the modern era, this is done automatically by the bundlers like rollup.js or webpack

With the globalThis proposal, this will no longer be needed and the global object will be standardized for us.

Conclusion

JavaScript is getting a lot of improvements, for sure. Some of them are more useful than others, but overall they are making our lives easier enabling us to write clean and maintainable code. I'm excited to see what does the future holds!

If you liked the article, consider buying me a coffee and subscribe here or follow me on twitter to stay tuned.

And as always, thanks for reading and see you in the next article.