Everything You Need to Know About ECMAScript 2018 (ES9)
Hey, JavaScript Lovers!
ECMAScript 2018, also known as ES2018 or ES9, is a version of the ECMAScript standard, which is the foundation for JavaScript. It was finalized in June 2018 and introduced several enhancements and new features to improve the language.
Some of the key features introduced in ES9 include:
- Asynchronous Iteration
- Rest/Spread Properties
- RegExp Enhancements
Promise.prototype.finally
JSON.stringify
Improvements- Function.prototype.toString
Object.prototype.__proto__
1. Asynchronous Iteration
Asynchronous iteration, introduced in ECMAScript 2018 (ES9), simplifies handling asynchronous operations. Before ES9, managing asynchronous data often involved nested callbacks or Promises, which could complicate and clutter the code. With the addition of asynchronous iteration, you can use for-await-of
loops to work with asynchronous data sources more intuitively and manageable.
Here’s a basic example of using for-await-of
to iterate over an asynchronous data source:
async function* fetchNumbers() {
// Simulates an asynchronous data source, like an API or database.
yield new Promise(resolve => setTimeout(() => resolve(1), 1000));
yield new Promise(resolve => setTimeout(() => resolve(2), 1000));
yield new Promise(resolve => setTimeout(() => resolve(3), 1000));
}
async function processNumbers() {
for await (const number of fetchNumbers()) {
console.log(number); // Logs 1, then 2, then 3, with a delay in between.
}
}
processNumbers();
Use Cases
Streaming Data:
Useful for handling data streams from APIs or other sources where you want to process data as it arrives.
Concurrent Asynchronous Operations:
Manage multiple asynchronous operations and process results one by one.
Improved Readability
It provides a more readable and maintainable way to work with asynchronous code compared to nested callbacks or chained Promises.
Reading Files Asynchronously
Here’s an example that reads lines from a file asynchronously:
const fs = require('fs');
const readline = require('readline');
async function* readLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
async function printLines(filePath) {
for await (const line of readLines(filePath)) {
console.log(line);
}
}
printLines('example.txt');
In this example:
readLines
is an async generator function that reads lines from a file.
The for-await-of
loop in printLines
iterates over each line as it’s read.
Asynchronous iteration simplifies the handling of streams and other asynchronous sequences, making your code cleaner and more maintainable.
2. Rest/Spread Properties
The rest and spread properties were enhanced to support object literals. This feature allows you to work with objects more flexibly and concisely, making it easier to manipulate and clone objects.
Rest Properties
Rest properties allow you to collect the remaining properties of an object after some properties have been extracted. This feature is particularly useful for creating new objects with only a subset of the properties from an existing object.
const person = {
name: 'John',
age: 30,
occupation: 'Engineer',
country: 'USA'
};
// Destructuring with rest properties
const { name, ...rest } = person;
console.log(name); // 'John'
console.log(rest); // { age: 30, occupation: 'Engineer', country: 'USA' }
Spread Properties
Spread properties are used to copy the properties of one object into another. This is useful for creating copies of objects or combining multiple objects into one.
const person = {
name: 'John',
age: 30
};
// Using spread properties to create a new object
const updatedPerson = {
...person,
occupation: 'Engineer'
};
console.log(updatedPerson); // { name: 'John', age: 30, occupation: 'Engineer' }
Practical Use Cases
1- Merging Objects
Merging properties from multiple objects into one.
const defaultSettings = { theme: 'light', layout: 'grid' };
const userSettings = { layout: 'list' };
const finalSettings = { ...defaultSettings, ...userSettings };
console.log(finalSettings); // { theme: 'light', layout: 'list' }
2- Cloning Objects
Creating a shallow copy of an object.
const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy); // { a: 1, b: 2 }
3- Updating Objects
Creating a new object with updated properties.
const person = { name: 'Alice', age: 25 };
const updatedPerson = { ...person, age: 26 };
console.log(updatedPerson); // { name: 'Alice', age: 26 }
4- Filtering Properties
Excluding certain properties from an object while copying.
const person = { name: 'Bob', age: 40, occupation: 'Developer' };
const { occupation, ...rest } = person;
console.log(rest); // { name: 'Bob', age: 40 }
Notes
Both rest and spread properties handle shallow copies. Nested objects or arrays within the object are not deeply copied.
The order of spread properties in object literals matters. Later properties can overwrite earlier ones.
3. RegExp Enhancements
ECMAScript 2018 (ES9) introduced several important enhancements to regular expressions (RegExp) that make pattern matching and manipulation more powerful and flexible. These improvements include the addition of new RegExp flags and methods that simplify and extend the capabilities of regular expressions.
RegExp Enhancements in ES9
Named Capture Groups:
Named capture groups allow you to name the groups in your regular expressions, making the result of a match easier to access and more readable.
Syntax: (?<name>...)
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const str = '2024-08-14';
const match = regex.exec(str);
if (match) {
console.log(match.groups.year); // '2024'
console.log(match.groups.month); // '08'
console.log(match.groups.day); // '14'
}
Named Groups in match
Method:
The match
method on strings now returns the named capture groups directly in the groups
property of the match object.
const str = '2024-08-14';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = str.match(regex);
if (result) {
console.log(result.groups.year); // '2024'
console.log(result.groups.month); // '08'
console.log(result.groups.day); // '14'
}
Lookbehind Assertions:
Lookbehind assertions enable you to match a pattern only if it is preceded by a specific preceding pattern. This is useful for more complex matching scenarios.
Syntax: (?<=...)
(positive lookbehind) and (?<!...)
(negative lookbehind)
const regexPositive = /(?<=\d{2})\d{2}/; // Match two digits preceded by two digits
const strPositive = '1234';
const matchPositive = strPositive.match(regexPositive);
console.log(matchPositive[0]); // '34'
const regexNegative = /(?<!\d{2})\d{2}/; // Match two digits not preceded by two digits
const strNegative = '1234';
const matchNegative = strNegative.match(regexNegative);
console.log(matchNegative[0]); // '12' (matches because it's not preceded by two digits)
Unicode Property Escapes:
Unicode property escapes provide a way to match characters based on their Unicode properties, such as their category or script.
Syntax: \p{Property=Value}
and \P{Property=Value}
// Match any character that is a Unicode letter
const regexLetter = /\p{L}/u;
console.log(regexLetter.test('A')); // true
console.log(regexLetter.test('1')); // false
// Match any character that is not a Unicode letter
const regexNotLetter = /\P{L}/u;
console.log(regexNotLetter.test('1')); // true
console.log(regexNotLetter.test('A')); // false
These enhancements make working with regular expressions more powerful and expressive, facilitating more complex and efficient text-processing tasks.
4. Promise.prototype.finally
In ECMAScript 2018 (ES9), the Promise.prototype.finally
method was introduced to improve handling of asynchronous operations by providing a way to execute code after a Promise
has settled, regardless of whether it was fulfilled or rejected.
The finally
method allows you to specify a callback that will be executed when a Promise
is settled, which means it has either been resolved or rejected. This can be useful for performing cleanup operations or for executing code that should run after the Promise
operation, regardless of its outcome.
How It Works
- When the
Promise
is fulfilled, thefinally
callback is executed after thethen
callback. - When the Promise is rejected, the finally callback is executed after the catch callback.
- The
finally
callback does not affect the resolution or rejection value of thePromise
.
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate success or failure
Math.random() > 0.5 ? resolve('Data loaded') : reject('Failed to load data');
}, 1000);
});
};
fetchData()
.then(data => {
console.log(data); // Handle success
})
.catch(error => {
console.error(error); // Handle failure
})
.finally(() => {
console.log('Operation complete'); // Always runs, regardless of success or failure
});
Practical Use Cases
Cleanup Operations:
Use finally
to perform cleanup operations regardless of whether an asynchronous task succeeds or fails. This can include things like hiding loading indicators or closing connections.
async function fetchData() {
const loadingIndicator = document.getElementById('loading');
loadingIndicator.style.display = 'block';
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
loadingIndicator.style.display = 'none'; // Hide loading indicator
}
}
Ensuring Code Execution:
Use finally
to ensure that certain code is executed after a Promise
has settled, such as logging or restoring state.
const processTask = async () => {
try {
await performTask();
} catch (error) {
console.error('Task failed:', error);
} finally {
console.log('Task processing complete');
}
};
Important Considerations
Chaining: finally
returns a Promise
that resolves to the same value or rejection as the original Promise
. This means it can be chained with then
and catch
.
fetchData()
.finally(() => console.log('Cleaning up'))
.then(() => console.log('Data processed'))
.catch(err => console.error('Error:', err));
No Arguments: The finally
callback does not receive any arguments and does not change the result of the original Promise
.
The finally
method enhances the flexibility of handling asynchronous operations by providing a way to ensure that certain actions are always performed, improving the reliability and cleanliness of your asynchronous code.
5. JSON.stringify Improvements
ECMAScript 2018 (ES9) introduced a useful enhancement to JSON.stringify()
: the addition of a replacer function. This feature allows for finer control over how objects are serialized, providing more flexibility in the serialization process.
JSON.stringify
Enhancements
Replacer Function:
When the replacer
parameter is a function, it is called for each key-value pair in the object being stringified. The function can return a new value or undefined
to omit the property from the JSON string.
Here’s an example of using a replacer function to filter out specific properties:
const person = {
name: 'Alice',
age: 30,
password: 'secret'
};
// Replacer function that omits the 'password' property
function replacer(key, value) {
if (key === 'password') {
return undefined; // Omit the password property
}
return value; // Keep other properties unchanged
}
const jsonString = JSON.stringify(person, replacer, 2);
console.log(jsonString);
// Output:
// {
// "name": "Alice",
// "age": 30
// }
In this example, the replacer
function is used to omit the password
property from the resulting JSON string.
Replacer Array
If you provide an array as the replacer
, only the properties listed in the array will be included in the resulting JSON string.
const person = {
name: 'Alice',
age: 30,
password: 'secret'
};
// Replacer array that includes only 'name' and 'age'
const jsonString = JSON.stringify(person, ['name', 'age'], 2);
console.log(jsonString);
// Output:
// {
// "name": "Alice",
// "age": 30
// }
In this example, only the name
and age
properties are included in the resulting JSON string, while password
is excluded.
6. Function.prototype.toString
In ECMAScript 2018 (ES9), the Function.prototype.toString
method was enhanced to return the exact source code of a function as a string. This enhancement improved the accuracy of the output from toString
for functions, especially in cases where functions are defined in a more complex or non-standard way.
The Function.prototype.toString
method returns a string representing the source code of the function. Prior to ES9, this method might not have returned the exact source code in some cases, particularly for functions defined with certain syntax or in certain environments.
Behavior in ES9
In ES9, toString
returns the exact source code of the function, including any whitespace or comments, as it appears in the source code. This means that if you define a function in a way that includes comments or is formatted with specific indentation, toString
will return that exact formatting.
Simple Function
function add(a, b) {
return a + b;
}
console.log(add.toString());
// Output:
// function add(a, b) {
// return a + b;
// }
In this example, toString
returns the exact source code of the add
function.
Function with Comments
/**
* Adds two numbers
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {number} The sum of the two numbers
*/
function add(a, b) {
return a + b;
}
console.log(add.toString());
// Output:
// /**
// * Adds two numbers
// * @param {number} a - The first number
// * @param {number} b - The second number
// * @returns {number} The sum of the two numbers
// */
// function add(a, b) {
// return a + b;
// }
Here, toString
includes the comments as part of the source code.
Arrow Function
const multiply = (x, y) => x * y;
console.log(multiply.toString());
// Output:
// (x, y) => x * y
The toString
method returns the exact source code of the arrow function.
Impact and Usage
Debugging and Logging:
Enhanced toString
is useful for debugging purposes where you need to log or inspect the exact source code of functions.
Code Analysis and Transformation:
Tools that analyze or transform code (e.g., minifiers or code formatters) can use the exact source code to better understand and manipulate functions.
Serialization:
In some cases, you might want to serialize functions for storage or transfer, and having the exact source code is beneficial.
Function Wrapping:
When wrapping functions (e.g., for logging or profiling), you might want to retain the original function’s source code for accurate representation.
Object.prototype.__proto__
In ECMAScript 2018 (ES9), the __proto__
property was standardized and formalized as a part of the language specification. Although __proto__
had been widely supported in various JavaScript engines prior to ES9, its standardization aimed to provide a consistent and reliable way to access and modify an object’s prototype.
The __proto__
property is an accessor property that provides a way to get or set the prototype of an object. It acts as a shortcut to interact with the internal [[Prototype]]
property, which is the actual mechanism JavaScript uses for prototypal inheritance.
Key Points
Getter and Setter:
- When used as a getter,
__proto__
returns the prototype of the object. - When used as a setter,
__proto__
sets the prototype of the object to the specified value.
Standardization:
Before ES9, __proto__
was widely implemented but was not part of the ECMAScript specification. ES9 formalized its behavior to ensure consistent implementation across different JavaScript engines.
Avoiding __proto__
:
While __proto__
is now standardized, it is generally recommended to use Object.getPrototypeOf()
and Object.setPrototypeOf()
for interacting with object prototypes. These methods provide a more explicit and standardized approach.
Examples
Getting the Prototype
const obj = {};
const proto = Object.create(null); // Create an object with no prototype
obj.__proto__ = proto;
console.log(obj.__proto__ === proto); // true
Setting the Prototype
const proto1 = { foo: 'bar' };
const proto2 = { baz: 'qux' };
const obj = {};
obj.__proto__ = proto1;
console.log(obj.foo); // 'bar'
obj.__proto__ = proto2;
console.log(obj.foo); // undefined (proto1 is no longer the prototype)
console.log(obj.baz); // 'qux'
Best Practices
Prefer Object.getPrototypeOf
and Object.setPrototypeOf
:
- For better readability and adherence to standards, use these methods instead of
__proto__
.
const obj = {};
const proto = { greet: 'hello' };
// Setting prototype using Object.setPrototypeOf
Object.setPrototypeOf(obj, proto);
console.log(obj.greet); // 'hello'
// Getting prototype using Object.getPrototypeOf
console.log(Object.getPrototypeOf(obj) === proto); // true
Avoid Changing Prototypes Dynamically:
- Changing an object’s prototype at runtime (e.g., using
__proto__
orObject.setPrototypeOf
) can lead to performance issues and should generally be avoided in favor of designing object hierarchies at object creation time.
If you missed my recent posts on ES6 (ECMAScript 2015), ES7 (ECMAScript 2016) and ES8 (ECMAScript 2017), you can catch up by clicking the links below to explore their features and updates.
Click here to explore ES6 (ECMAScript 2015)
Click here to explore ES7 (ECMAScript 2016)
Click here to explore ES8 (ECMAScript 2017)
Happy coding!
FAQs
How do Array.prototype.flat() and Array.prototype.flatMap() work?
Array.prototype.flat(depth)
flattens nested arrays into a single array. Thedepth
argument specifies how deep a nested array structure should be flattened. The default depth is 1.Array.prototype.flatMap(callback)
first maps each element using a mapping function, then flattens the result into a new array.
let arr = [1, [2, [3, [4]]]];
console.log(arr.flat(2)); // [1, 2, 3, [4]]
let arr2 = [1, 2, 3, 4];
console.log(arr2.flatMap(x => [x * 2])); // [2, 4, 6, 8]
What is Object.fromEntries() used for?
Object.fromEntries()
is used to convert a list of key-value pairs (such as an array of arrays or a Map) into an object.
let entries = [['name', 'John'], ['age', 30]];
let obj = Object.fromEntries(entries);
console.log(obj); // { name: 'John', age: 30 }
What are String.prototype.trimStart() and String.prototype.trimEnd()?
String.prototype.trimStart()
removes whitespace from the beginning of a string.String.prototype.trimEnd()
removes whitespace from the end of a string.
let str = ' Hello World! ';
console.log(str.trimStart()); // 'Hello World! '
console.log(str.trimEnd()); // ' Hello World!'
What is the Optional Catch Binding feature?
The Optional Catch Binding feature allows you to omit the error parameter in a catch
block if it is not needed. This helps simplify code when the error is not used.
try {
// Code that may throw an error
} catch {
// Error handling without using the error parameter
}
How did Function.prototype.toString() improve in ES9?
In ES9, Function.prototype.toString()
was standardized to return a more consistent and reliable source code representation of the function. This is particularly useful for debugging and development tools.
function example() { return 42; }
console.log(example.toString()); // 'function example() { return 42; }'
What changes were made to JSON.stringify() in ES9?
ES9 improved the handling of certain edge cases in JSON.stringify()
, ensuring that it correctly handles special scenarios like objects with a Symbol.toJSON
method.
How can I check if my environment supports ECMAScript 2019 features?
You can use feature detection libraries such as Babel to transpile code for compatibility with older environments. Additionally, check compatibility tables on resources like MDN Web Docs or Can I use for specific feature support.
4 thoughts on “Everything You Need to Know About ECMAScript 2018 (ES9)”