10 ES6+ JavaScript Features That Will Make Your Code _Actually_ Readable


JavaScript has come a long way since its early days. Remember when we had to use var
for everything and concatenate strings with +
signs? Shudders. If you're still writing JavaScript like it's 2014, you're not just missing out on some syntactic sugar - you're missing fundamental improvements that make your code cleaner, more expressive, and way less error-prone.
I've spent the last decade watching JavaScript evolve, and I still get excited about these modern features. Let's dive into the good stuff that ES6 and beyond brought us, with practical examples you can start using in your code today.
1. Destructuring: Extracting Values Without the Fuss
Ever find yourself writing the same property access patterns over and over? Destructuring is like a shortcut that lets you unpack values from objects and arrays in a single elegant line.
Object Destructuring
Before ES6, accessing object properties required repetitive code:
// The old way 🥱
const user = {
name: "Alex",
age: 28,
location: "Berlin",
};
const name = user.name;
const age = user.age;
console.log(name, age); // Alex 28
With destructuring, this becomes beautifully concise:
// The modern way ✨
const { name, age } = user;
console.log(name, age); // Alex 28
// You can even rename properties
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // Alex 28
Array Destructuring
This works for arrays too, based on position rather than names:
const rgb = [255, 140, 0];
// Old way
const red = rgb[0];
const green = rgb[1];
const blue = rgb[2];
// Modern way
const [red, green, blue] = rgb;
console.log(`RGB: ${red}, ${green}, ${blue}`); // RGB: 255, 140, 0
Did you know you can even use destructuring in function parameters? This makes your functions so much more readable:
// Instead of this
function createUser(userData) {
const username = userData.username;
const email = userData.email;
// ...
}
// You can do this
function createUser({ username, email }) {
// Now username and email are directly available!
console.log(`Creating ${username} with email ${email}`);
}
2. Template Literals: The String Revolution You Didn't Know You Needed
Remember string concatenation? That awkward dance of quotes, plus signs, and escaped newlines?
// The old string concatenation horror show
var greeting =
"Hello, " + userName + "!\n" + "Your last login was on " + lastLogin + ".";
Template literals fixed all of that with backticks () and
${}` for expressions:
// Clean, readable, maintainable
const greeting = `Hello, ${userName}!
Your last login was on ${lastLogin}.`;
The benefits are massive:
Multi-line strings without escape characters
Expression interpolation with
${}
Improved readability, especially for HTML templates
Here's a real-world example where template literals shine - generating HTML:
const products = [
{ name: "Laptop", price: 999 },
{ name: "Phone", price: 699 },
];
const productList = `
<ul class="product-list">
${products
.map(
(product) => `
<li class="product">
<h3>${product.name}</h3>
<p>$${product.price}</p>
</li>
`
)
.join("")}
</ul>
`;
document.body.innerHTML = productList;
Notice how readable that is compared to the string concatenation nightmare it would have been before?
3. Spread and Rest Operators: The Triple-Dot Magic
The ...
operator is probably one of JavaScript's most versatile additions. It serves two distinct but related purposes:
Spread: Expanding Values
The spread operator unpacks elements from arrays or properties from objects. It's like saying "take all this stuff and put it here."
Here's where it shines:
// Combining arrays
const oldTeam = ["Sarah", "David"];
const newTeam = ["Alex", ...oldTeam, "Maria"];
console.log(newTeam); // ['Alex', 'Sarah', 'David', 'Maria']
// Copying objects and overriding properties
const defaultSettings = {
theme: "light",
notifications: true,
fontSize: "medium",
};
const userSettings = {
...defaultSettings,
theme: "dark", // Override just the theme
};
Did you catch that? We created a new object that inherits all the properties of the default settings but overrides just the theme. No mutation, no complex merging logic.
Rest: Collecting Values
The rest operator does the opposite - it collects multiple elements into an array. It's incredibly useful for functions that can accept a variable number of arguments:
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
It's also perfect for capturing "the rest" of an array when destructuring:
const [first, second, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(others); // [3, 4, 5]
4. Map and Set: Collections That Actually Make Sense
JavaScript needed better data structures, and ES6 delivered with Map
and Set
.
Map: When Objects Just Don't Cut It
A Map
is like an object on steroids. While objects are limited to string keys and come with prototype baggage, Maps can use any value as a key (even objects!) and are designed specifically for frequent additions and removals:
// Creating a Map
const userRoles = new Map();
// Adding entries
userRoles.set("john", "admin");
userRoles.set("lisa", "editor");
// Objects as keys (try doing THIS with a regular object!)
const teamA = { name: "Team A" };
const teamB = { name: "Team B" };
userRoles.set(teamA, "projects");
userRoles.set(teamB, "marketing");
// Checking membership and retrieval
console.log(userRoles.has("john")); // true
console.log(userRoles.get(teamA)); // 'projects'
// Iteration - notice the destructuring!
for (const [user, role] of userRoles) {
console.log(`${user} is a ${role}`);
}
Fun fact: Maps maintain insertion order when you iterate over them, unlike objects!
Set: For Unique Values Only, Please
A Set
is an ordered collection of unique values - perfect for removing duplicates:
// Creating a set from an array (duplicates automatically removed)
const tags = new Set(["javascript", "tutorial", "es6", "javascript", "web"]);
console.log(tags); // Set(4) {'javascript', 'tutorial', 'es6', 'web'}
// Adding and checking values
tags.add("programming");
console.log(tags.has("es6")); // true
// Converting back to array if needed
const uniqueTags = [...tags];
Here's a clever one-liner to remove duplicates from an array:
const uniqueArray = [...new Set(duplicatedArray)];
5. Arrow Functions: Concise and Context-Aware
Arrow functions aren't just shorter - they also handle the this
keyword differently, making callback functions much more intuitive:
// Old anonymous function
const oldFunction = function (x) {
return x * 2;
};
// Arrow function
const arrowFunction = (x) => x * 2;
// Multiline arrow function
const multiline = (x, y) => {
const sum = x + y;
return sum * 2;
};
The biggest benefit is in callbacks where this
previously caused countless bugs:
// Pre-ES6: The notorious 'this' problem
function Counter() {
this.count = 0;
setInterval(function () {
// 'this' doesn't refer to Counter instance!
this.count++; // Doesn't work
console.log(this.count);
}, 1000);
}
// ES6: Arrow functions preserve 'this'
function Counter() {
this.count = 0;
setInterval(() => {
// 'this' refers to the Counter instance
this.count++; // Works perfectly
console.log(this.count);
}, 1000);
}
Arrow functions have transformed how JavaScript developers handle callbacks and functional programming patterns.
6. Default Parameters: No More Parameter Checking
Remember writing this defensive code?
// Old way
function createProfile(name, age, isPremium) {
name = name || "Anonymous";
age = age || 30;
isPremium = isPremium !== undefined ? isPremium : false;
// function body
}
With default parameters, it's much cleaner:
// ES6 way
function createProfile(name = "Anonymous", age = 30, isPremium = false) {
// function body
}
// You can call it with just some parameters
createProfile("Sarah"); // Other params use defaults
This might seem like a small improvement, but over hundreds of functions in a codebase, it adds up to significant readability and fewer bugs.
7. Classes: Object-Oriented JavaScript That Makes Sense
JavaScript has always had object-oriented features, but the syntax was... quirky. ES6 classes provide a cleaner, more familiar way to create object blueprints:
// Old prototype-based inheritance
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(this.name + " makes a noise.");
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
console.log(this.name + " barks.");
};
// ES6 class syntax
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
Under the hood, it's still prototypal inheritance, but the syntax is so much more readable and maintainable.
8. Modules: Proper Code Organization At Last
Before ES6, we had various module patterns, AMD, CommonJS, and other workarounds. Now, JavaScript has native module support:
// math.js - Export functions
export function add(x, y) {
return x + y;
}
export function multiply(x, y) {
return x * y;
}
// Or export as default
export default class Calculator {
// Class implementation
}
// main.js - Import functions
import { add, multiply } from "./math.js";
import Calculator from "./math.js";
console.log(add(2, 3)); // 5
This makes code organization vastly better and enables proper dependency management without bundlers (though we still use them for browser compatibility).
9. Promises and Async/Await: Taming Asynchronous Code
Callback hell was a real problem in JavaScript. Promises provided a structured way to handle asynchronous code:
// Callback hell example
fetchUser(
userId,
function (user) {
fetchUserPosts(
user.id,
function (posts) {
fetchPostComments(
posts[0].id,
function (comments) {
// Deeply nested and difficult to manage
console.log(comments);
},
handleError
);
},
handleError
);
},
handleError
);
// Promise chain
fetchUser(userId)
.then((user) => fetchUserPosts(user.id))
.then((posts) => fetchPostComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((error) => handleError(error));
And then ES2017 gave us async/await, which makes async code look almost synchronous:
async function getUserComments() {
try {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(user.id);
const comments = await fetchPostComments(posts[0].id);
console.log(comments);
} catch (error) {
handleError(error);
}
}
This transformation has massively improved code readability and error handling.
10. Optional Chaining: The End of Property Access Nightmares
This ES2020 feature might be the most underrated quality-of-life improvement. Before optional chaining, accessing nested properties was defensive programming hell:
// The old defensive way
const streetName =
user &&
user.address &&
user.address.details &&
user.address.details.street &&
user.address.details.street.name;
// With optional chaining
const streetName = user?.address?.details?.street?.name;
Combined with nullish coalescing (??
), it's even more powerful:
// Use default only if property is null or undefined
const username = user?.name ?? "Anonymous";
This feature alone has probably prevented thousands of "Cannot read property 'x' of undefined" errors across the JavaScript ecosystem.
Wrapping Up: The ES6+ Impact
Modern JavaScript isn't just about writing fewer lines of code - it's about writing code that better expresses your intent. It's more declarative (saying what you want, not how to do it) and reduces cognitive load.
Ever notice how the best code reads almost like natural language? That's what these features enable. You're no longer fighting with the syntax - you're expressing your ideas directly.
These features have fundamentally changed how JavaScript developers work. If you haven't fully embraced them yet, now's the time. Your future self (and your team) will thank you.
What was your favorite ES6+ feature? Did I miss any that revolutionized your coding style? Drop a comment below - I'd love to hear which modern JavaScript features you find most valuable in your daily work.
PS: For those wondering, I didn't even get to cover everything - there are even more goodies like generators, for...of loops, and Object.entries() that came in ES6 and later versions. Maybe that's for another post?
Subscribe to my newsletter
Read articles from Jatin Verma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
