Modern JavaScript Features (ES6+)
ECMAScript 2015 (ES6) and subsequent versions introduced many powerful features that transformed JavaScript development. These modern features make code more concise, readable, and maintainable while providing new capabilities for solving complex problems.
Introduction to ECMAScript Versions
ECMAScript is the standardized specification for JavaScript. Since 2015, new versions have been released annually, each adding new features to the language.
Version | Year | Key Features |
---|---|---|
ES6 / ES2015 | 2015 | Arrow functions, classes, template literals, destructuring, let/const, promises, modules |
ES2016 | 2016 | Array.includes(), exponentiation operator (**) |
ES2017 | 2017 | Async/await, Object.entries(), Object.values(), string padding |
ES2018 | 2018 | Rest/spread for objects, async iteration, Promise.finally() |
ES2019 | 2019 | Array.flat(), Array.flatMap(), Object.fromEntries(), String.trimStart()/trimEnd() |
ES2020 | 2020 | Optional chaining, nullish coalescing, BigInt, Promise.allSettled() |
ES2021 | 2021 | String.replaceAll(), Promise.any(), logical assignment operators |
ES2022 | 2022 | Class fields, top-level await, Array.at(), Object.hasOwn() |
ES6 (ES2015) Features
Arrow Functions
Arrow functions provide a concise syntax for writing functions and lexically bind the this
value.
// Traditional function
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;
// Arrow function with block body
const add = (a, b) => {
const sum = a + b;
return sum;
};
// Arrow function with single parameter (parentheses optional)
const double = x => x * 2;
// Arrow function with no parameters
const getRandomNumber = () => Math.random();
Note: Arrow functions don't have their own this
context. They inherit this
from the enclosing scope, which can be useful in callbacks but problematic in object methods.
let and const Declarations
Block-scoped variable declarations that replace var
for most use cases.
// let - block-scoped variable that can be reassigned
let count = 1;
count = 2; // Valid
// const - block-scoped variable that cannot be reassigned
const PI = 3.14159;
// PI = 3.14; // Error: Assignment to constant variable
// Block scope
if (true) {
let blockScoped = 'only available in this block';
const alsoBlockScoped = 'also only in this block';
var notBlockScoped = 'available outside the block';
}
// console.log(blockScoped); // ReferenceError
// console.log(alsoBlockScoped); // ReferenceError
console.log(notBlockScoped); // Works
Tip: Use const
by default, and only use let
when you need to reassign a variable. This makes your code more predictable and easier to reason about.
Template Literals
String literals that allow embedded expressions and multi-line strings.
// String concatenation (old way)
const name = 'John';
const greeting = 'Hello, ' + name + '!';
// Template literals (new way)
const greeting = `Hello, ${name}!`;
// Multi-line strings
const multiLine = `This is a string
that spans multiple
lines without needing \n`;
// Expressions in template literals
const a = 5;
const b = 10;
console.log(`The sum of ${a} and ${b} is ${a + b}`);
Destructuring Assignment
Extract values from arrays or objects into distinct variables.
// Array destructuring
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// Object destructuring
const person = {
name: 'John',
age: 30,
city: 'New York'
};
const { name, age } = person;
console.log(name, age); // 'John', 30
// Renaming variables
const { name: fullName, age: years } = person;
console.log(fullName, years); // 'John', 30
// Default values
const { name, job = 'Unknown' } = person;
console.log(job); // 'Unknown'
Rest and Spread Operators
Collect multiple elements into an array (rest) or expand an array into individual elements (spread).
// Rest operator in function parameters
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
// Spread operator with arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Spread operator to clone an array
const original = [1, 2, 3];
const copy = [...original];
Note: In ES2018, the rest and spread operators were extended to work with objects as well.
Classes
Syntactic sugar over JavaScript's prototype-based inheritance.
// ES6 Class
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
static createAnonymous() {
return new Person('Anonymous', 0);
}
}
const john = new Person('John', 30);
console.log(john.greet()); // "Hello, my name is John"
// Inheritance
class Employee extends Person {
constructor(name, age, jobTitle) {
super(name, age); // Call parent constructor
this.jobTitle = jobTitle;
}
greet() {
return `${super.greet()} and I work as a ${this.jobTitle}`;
}
}
Promises
A built-in way to handle asynchronous operations.
// Creating a promise
const promise = new Promise((resolve, reject) => {
// Asynchronous operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation successful');
} else {
reject('Operation failed');
}
}, 1000);
});
// Using a promise
promise
.then(result => {
console.log(result); // "Operation successful"
return 'Next step';
})
.then(result => {
console.log(result); // "Next step"
})
.catch(error => {
console.error(error);
});
ES2016-ES2022 Features
Array.prototype.includes()
Check if an array includes a certain element.
const numbers = [1, 2, 3, 4, 5];
// Old way
console.log(numbers.indexOf(3) !== -1); // true
// ES2016 way
console.log(numbers.includes(3)); // true
console.log(numbers.includes(6)); // false
Async/Await
Syntactic sugar for working with promises, making asynchronous code look more like synchronous code.
// Promise-based approach
function fetchData() {
return fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log(data);
return data;
})
.catch(error => {
console.error('Error:', error);
throw error;
});
}
// Async/await approach
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
Optional Chaining
Safely access nested object properties without checking if each level exists.
const user = {
name: 'John',
address: {
street: '123 Main St',
city: 'New York'
}
};
// Old way
const city = user && user.address && user.address.city;
// ES2020 way
const city = user?.address?.city;
// Works with function calls too
const result = user.getDetails?.();
// Works with array elements
const firstItem = array?.[0];
Nullish Coalescing Operator
Provides a default value only when the left-hand side is null or undefined.
// Old way with logical OR
// Problem: returns fallback for all falsy values (0, '', false, null, undefined)
const count = data.count || 0;
// ES2020 way with nullish coalescing
// Only returns fallback for null or undefined
const count = data.count ?? 0;
// Examples
console.log(0 ?? 42); // 0 (0 is not nullish)
console.log('' ?? 'default'); // '' (empty string is not nullish)
console.log(false ?? true); // false (false is not nullish)
console.log(null ?? 42); // 42 (null is nullish)
console.log(undefined ?? 42); // 42 (undefined is nullish)
Top-level await
Use await outside of async functions in modules.
// In a module, you can now use await at the top level
// Without wrapping it in an async function
// ES2022
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
// Export the data for other modules to use
export { data };
Next Steps
Now that you understand modern JavaScript features, you can explore:
- JavaScript modules and module bundlers
- Advanced patterns with modern JavaScript
- Functional programming in JavaScript
- TypeScript - a typed superset of JavaScript
- Modern JavaScript frameworks that leverage these features