Ever since ES2015 was released, which was a great leap forward in itself, JavaScript has been improving at a fast pace. Every year since then, new features have been added to JavaScript specifications.
Features like new syntax and new methods for building in JavaScript have been added consistently. In ES2016 and 2017, The Object
object had methods like Object.values
and Object.entries
added.
String methods like padStart
and padEnd
were added in ES2017. async
and await
, which is a shorthand syntax for chaining promises, were also added in ES2017.
The includes
methods for arrays were added in ES2016. ES2018 was another release with lots of new features. With ES2018, the spread syntax is now available for object literals. Rest parameters were also added.
The for await...of
loop, which is a loop syntax iterating through promises sequentially, was also added. The SharedArrayBuffer
object was added for representing raw binary data that cannot become detached.
A finally
function was also added to the Promise
object.
Spread Operator in Objects
The spread syntax works by copying the values of the original array, and then inserting them into another array or putting them in the order they appeared in the array as the list of arguments in a function in the same order.
When the spread operator is used with objects, the key-value pairs appear in the same order they appeared in the original object.
With ES2018, the spread operator works with object literals. Then key-value pairs of an object can be inserted into another object with the spread operator.
If there are two objects with the same key that the spread operator is applied to in the same object, the one that’s inserted later will overwrite the one that’s inserted earlier.
For example, if we have the following:
let obj1 = {foo: 'bar', a: 1};
let obj2 = {foo: 'baz', b: 1};
let obj3 = {...obj1, ...obj2 }
Then we get {foo: “baz”, a: 1, b: 1}
as the value of obj3
because obj1
is spread before obj2
. They both have foo
as a key in the object.
First foo: 'bar'
is inserted by the spread operator to obj3
. Then foo: 'baz'
overwrites the value of foo
after obj2
is merged in since it has the same key, foo
, but was inserted later.
This is great for merging objects as we don’t have to loop through the keys and put in the values, which is much more than one line of code.
One thing to note is that we can’t mix the spread operator between regular objects and iterable objects. For example, we will get TypeError
if we write the following:
let obj = {foo: 'bar'};
let array = [...obj];
Rest Operator
The rest operator is a JavaScript operator where we can store an indefinite number of arguments in a function as an array. It looks exactly like the spread operator, which lets us spread entries of an array or a list of key-value pairs of an object into another object.
For example, if we have a function that has a long list of arguments, we can use the rest operator to shorten the list of parameters in a function.
The rest operator is used with the following syntax:
const fn = (a,b,..restOfTheArgs) => {...}
Where restOfTheArgs
is an array with the list of arguments other than the first two.
For example, if we want to write a sum
function that takes an indefinite list of numbers as arguments and sum up the numbers, we can write:
const sum = (a,b,...otherNums) => {
return a + b + otherNums.reduce((x,y)=>x+y, 0);
}
As we can see, this is very handy for functions that have any list of arguments. Before we had this, we had to use the arguments
object available in functions to get a list of arguments. This is not ideal as we allow them to pass in anything into the arguments.
With the rest operator, we have the best of both worlds. We can have some fixed parameters, while the rest stay flexible. This makes functions more flexible than functions with a fixed number of parameters, while having some flexibility of functions that take an indefinite number of arguments.
The arguments
object has all the arguments of the function. Also, it’s not a real array, so array functions aren’t available to them. It’s just an object with indexes and keys to denote the argument’s positions.
Methods like sort, map, forEach, or pop cannot be run on the argument
’s object. It also has other properties. This creates confusion for programmers. The arguments that are converted to an array with the rest operator do not have these issues, as they are real arrays.
To call the sum
function we wrote, we can write:
const result = sum(1,2,3,4,5,6,7,8,9,10);
The result
will be 55, since we summed up all the arguments together. otherNums
is an array with all the numbers other than 1 and 2.
We can also use the rest operator to destructure a list of arguments into a list of variables. This means that we can convert a list of parameters into an array with the spread operator, and then decompose the array of arguments into a list of variables.
This is very useful as we can get the entries of the array that’s operated on by the rest operator and convert them to named variables. For example, we can write:
const sum = (a,b,...[c,d,e])=> a+b+c+d+e;
This way, we can use the rest operator, but limit the number of arguments that your function accepts. We take the function parameters a
and b
, and we also take c
, d
, and e
as parameters.
However, it’s probably clearer without using the rest operator since all the parameters are fixed, we can just list the parameters directly.
for await...of
The for await...of
loop allows us to create a loop that iterates over a list of promises as well as over the entries of a normal iterables.
It works with iterable objects like arrays, string, argument
object, NodeList
object, TypedArray
, Map
, Set
, and user-defined synchronous and asynchronous iterable objects.
To use it, we write:
for await (let variable of iterable) {
// run code on variable
}
The variable
may be declared with const
, let
, or var
, and the iterable is the iterable objects that we are iterating over. We can iterate over asynchronous iterable objects like the following:
const asynNums = {
[Symbol.asyncIterator]() {
return {
i: 6,
next() {
if (this.i < 20) {
return Promise.resolve({
value: this.i++,
done: false
});
}
return Promise.resolve({
done: true
});
}
};
}
};
(async () => {
for await (let num of asynNums) {
console.log(num);
}
})();
We should see 6 to 19 logged. It also works with async generators:
async function* asynNumGenerator() {
var i = 6;
while (i < 20) {
yield i++;
}
}
(async () => {
for await (let num of asynNumGenerator()) {
console.log(num);
}
})();
We should see the same thing logged. It also works great with promises:
const arr = Array.from({
length: 20
}, (v, i) => i)let promises = [];
for (let num of arr) {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num);
}, 100)
})
promises.push(promise);
}
(async () => {
for await (let num of promises) {
console.log(num);
}
})();
As we can see, if we run the code above, we should see 0 to 19 logged sequentially, which means that promises were iterated through in sequence. This is very handy as we never have anything that can iterate through asynchronous code before this loop syntax was introduced.
Promise.finally()
The finally
function is added to the Promise
object which runs when the promise is settled. That means it runs whenever the promise is fulfilled or rejected.
It takes a callback function that runs whenever the promise is settled. This lets us run code regardless of how a promise ends. Also, this means that we no longer have to duplicate code in the callbacks for then
and catch
functions.
To use the finally
function, we use it as the following:
promise
.finally(()=>{
// run code that when promise is settled
})
The finally
is useful is we want to run code whenever a promise ends like cleanup code or processing after the code ends.
It’s very similar to calling .then(onFinally, onFinally)
. However, we do not have to declare it twice to make a variable for it and pass it in twice. The callback for the finally
function doesn’t take any argument, as there’s no way to determine the status of the promise beforehand.
It’s used for cases when we don’t know what will happen to the promise, so there’s no need to provide any argument. If a promise is fulfilled then the resolved value will be intact after calling the finally
function.
For example, if we have:
Promise.resolve(3).then(() => {}, () => {})
This will resolve to undefined
, but if we have:
Promise.resolve(3).finally(() => {})
The code above will resolve to 3. Similarly, if the promise if rejected, we get:
Promise.reject(5).finally(() => {})
This will be rejected with the value 5.
SharedArrayBuffer
The SharedArrayBuffer
is an object that’s used to represent a fixed-length raw binary data buffer.
It’s similar to the ArrayBuffer
object, but we can use it to create views on shared memory. It can’t become detached, unlike an ArrayBuffer
. The constructor of it takes the length of the buffer as an argument.
The length is the size in byte for the array buffer to create. It returns a SharedArrayBuffer
of the specified size with contents initialized to 0.
Shared memory can be created and updated by multiple workers on the main thread. However, this may take a while until the changes are propagated to all contexts.
Conclusion
With ES2018, we have more handy features to help developers develop JavaScript apps.
The highlights include the spread operator for objects, which let us copy key-value pairs to other objects and the rest operator to let us pass optional arguments.
The for await...of
loop lets us iterate through collections of asynchronous code which could never easily be done before.