Like any other programming language, JavaScript has its own list of best practices to make programs easier to read and maintain. There are a lot of tricky parts to JavaScript, and we can follow some best practices to improve our code.
Since ES6 was introduced, new constructs are replacing older ones for good reasons. It’s much shorter, cleaner, and easier to understand.
In this article, we’ll look at which older constructs that can be replaced with new ones, including replacing then
with async
and await
, replacing dictionary objects with Map
s, replacing apply
with the spread operator, and replacing function constructors with the class syntax.
Replace Then with Async / Await
When we chain promises, we used to do it by using the then
method and then returning another promise in the callback function that we pass into then
.
This means that we have code that looks something like this:
Promise.resolve(1)
.then((val) => {
console.log(val);
return Promise.resolve(2);
})
.then((val) => {
console.log(val);
return Promise.resolve(3);
})
.then((val) => {
console.log(val);
})
With the introduction of the async
and await
syntax, which is just a shorthand for calling the then
method repeatedly. We can write the code above like this:
(async () => {
const val1 = await Promise.resolve(1);
console.log(val1);
const val2 = await Promise.resolve(2);
console.log(val2);
const val3 = await Promise.resolve(3);
console.log(val3);
})();
They both output 1, 2 and 3, but the second code snippet is so much shorter. It’s so much clearer that there’s no reason to go back to the old syntax.
Also, we can loop through promises and run them one after the other by using the for-await-of
loop. We can loop through the promises we have above by rewriting the code like the following:
(async () => {
const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
]
for await (let p of promises) {
const val = await p;
console.log(val);
}
})();
Code like the one we have above is very handy for looping through many promises or promises that are created on the fly, which we can’t do in the earlier examples.
Replacing Dictionary Objects with Maps
ES6 also introduced the Map
constructor, which lets us create hash maps or dictionaries without using JavaScript objects with string keys to do so.
Map
s are also better because they have their own methods to get and manipulate the entries.
For example, instead of writing:
const dict = {
'foo': 1,
'bar': 2
}
We write:
const dict = new Map([
['foo', 1],
['bar', 2]
])
Then we can get an entry by using the get
method as follows:
console.log(dict.get('foo'));
We can set the value of an existing entry by the key of the entry with the set
method:
dict.set('foo', 2);
Also, we can check if an entry exists with the given key with the has
method:
dict.has('baz');
There are also the keys
and entries
methods to get all the keys of the map and all the entries respectively.
For example, we can write:
console.log(dict.keys());
To get an iterator object with the keys of the Map
. This means that we can loop through them with the for...of
loop or convert it to an array with the spread operator.
Similarly, the entries
method returns an iterator object with all the entries in the Map
with each entry being an array of [key, value]
.
There’s also the value
method to get an iterator object with all the values in the Map
.
We can also use other primitive values as keys. If we use objects, we can’t get the value back when we look them up since the lookup is done with the ===
operator which returns false
is 2 objects that don’t have the same reference even if they have the same content.
These methods aren’t available in a regular object. Also, we might accidentally get or modify the object’s prototype’s properties instead of its own if we use the for...in
loop.
Therefore, there aren’t many reasons to use a regular object as a dictionary anymore.
Photo by David Clode on Unsplash
Replace Apply with Spread
If we don’t want to change the value of this
inside the function, there isn’t much reason to use the apply
method in functions.
If we only want to call a function with many arguments, we can use the spread operator when passing in an array for our arguments as follows:
const arr = [1, 2, 3, 4, 5];
const add = (...args) => args.reduce((a, b) => a + b, 0);
console.log(add(...arr));
All we have to do is to use the spread operator on our array, which is the 3 dots operator in the last line, and then the array will be separated into a comma-separated list of arguments.
Replace Constructor Functions with Classes
Another great ES6 feature is the class syntax for constructor functions. It’s simply syntactic sugar that makes the constructor function looks like a class.
The advantage of it is that it makes inheritance easy.
For example, if we want to inherit from a constructor function, we have to write something like this:
function Person(name) {
this.name = name;
}
function Employee(name, employeeCode) {
Person.call(this, name);
Employee.prototype.constructor = Person;
this.employeeCode = employeeCode;
}
const employee = new Employee('Joe', 1);
console.log(employee)
This syntax looks strange coming from class-based object-oriented languages like Java.
However, the class syntax makes things look a lot familiar to developers that used other languages more than JavaScript. We can rewrite the code above to the following:
class Person {
constructor(name) {
this.name = name;
}
}
class Employee extends Person {
constructor(name, employeecode) {
super(name);
this.employeecode = employeecode;
}
}
const employee = new Employee('Joe', 1);
console.log(employee)
The code does the same thing as what we have above. However, it’s clearer what we’re inheriting from since we have the extends
keyword to indicate what we’re inheriting from.
With the constructor function, we have to worry about the value of this
in the first argument of the call
method, and what we pass into the subsequent arguments of call
.
With the class syntax, we don’t have to worry about this. If we forgot to make the super
call like the following code:
class Person {
constructor(name) {
this.name = name;
}
}
class Employee extends Person {
constructor(name, employeecode) {
this.employeecode = employeecode;
}
}
const employee = new Employee('Joe', 1);
console.log(employee)
We’ll get the error ‘Uncaught ReferenceError: Must call super
constructor in derived class before accessing ‘this’ or returning from derived constructor.’
It’s one less chance of making a mistake.
We don’t get any error if we omit the Person.call
like in the Employee
constructor function since the browser doesn’t know we want Employee
to inherit from Person
.
In addition, when we log the prototype of employee
, we get that the prototype of employee
is Person
as expected with the class syntax.
However, we don’t get that unless we put Employee.prototype.constructor = Person;
which is easily forgotten.
Conclusion
async
and await
and for-await-of
are new constructs that make chaining promises much cleaner. It’s much better to use them instead of using then
because of it.
for-await-of
also lets us loop through promises that are generated dynamically which we can’t do with then
or async
and await
alone.
Map
s are much better than plain objects for hashes and dictionaries because it has its own methods to manipulate and get the entries. Also, we may accidentally be accessing the properties of prototype of plain objects.
If we don’t want to change the value of this
inside a function, we can replace the apply
method for calling functions with an array of arguments with the spread operator, which does the same thing.
Finally, the class syntax for constructors is much better than the original function syntax since we can inherit from parent classes easier than setting the prototype constructor of a constructor function.