The Complete Guide to Modern JavaScript ES6 Features
A comprehensive overview of ES6 (ES2015) features including let/const, arrow functions, template literals, destructuring, promises, async/await, classes, and modules, with practical examples for modern JavaScript development.
Advertisement
If you've been writing JavaScript for a while, you probably remember the days when var was the only way to declare variables, and loops required verbose counter variables. Then ES6 (also called ES2015) came along and changed everything. It wasn't just an update—it was a revolution that made JavaScript feel like a completely new language.
At PythonSkillset, we've seen developers struggle with the transition from old-school JavaScript to modern ES6 syntax. But once you understand these features, you'll wonder how you ever lived without them. Let's break down the most important ones.
Let and Const: Say Goodbye to Var
The old var keyword had some quirks. It was function-scoped, not block-scoped, which led to confusing bugs. ES6 introduced let and const to fix this.
// Old way with var
var x = 10;
if (true) {
var x = 20; // This overwrites the outer x!
}
console.log(x); // 20
// Modern way with let
let y = 10;
if (true) {
let y = 20; // This is a different variable
}
console.log(y); // 10
Use const for values that shouldn't change, and let for everything else. This makes your code more predictable and easier to debug.
Arrow Functions: Cleaner Syntax, Clearer Intent
Arrow functions aren't just about typing fewer characters. They also handle the this keyword differently, which solves a common headache.
// Old way
const numbers = [1, 2, 3];
const doubled = numbers.map(function(n) {
return n * 2;
});
// ES6 way
const doubled = numbers.map(n => n * 2);
The real magic happens with this. In regular functions, this changes based on how the function is called. Arrow functions inherit this from their surrounding scope, which is usually what you want.
// Without arrow functions, you'd need a workaround
function Counter() {
this.count = 0;
setInterval(function() {
this.count++; // 'this' is wrong here
}, 1000);
}
// With arrow functions, it just works
function Counter() {
this.count = 0;
setInterval(() => {
this.count++; // 'this' refers to the Counter instance
}, 1000);
}
Template Literals: Strings That Actually Make Sense
Remember concatenating strings with + and worrying about spaces? Template literals use backticks and ${} to embed expressions directly.
const name = "PythonSkillset";
const year = 2025;
// Old way
const message = "Welcome to " + name + " in " + year + "!";
// ES6 way
const message = `Welcome to ${name} in ${year}!`;
You can also create multi-line strings without \n:
const bio = `
PythonSkillset is your go-to resource
for learning modern JavaScript and Python.
We cover everything from basics to advanced topics.
`;
Destructuring: Extract Data with Style
Destructuring lets you unpack values from arrays or properties from objects into distinct variables. It's like having a shortcut for data extraction.
// Array destructuring
const [first, second, third] = [10, 20, 30];
console.log(first); // 10
// Object destructuring
const user = { name: "PythonSkillset", role: "admin" };
const { name, role } = user;
console.log(name); // "PythonSkillset"
This is incredibly useful when working with API responses or function parameters:
function displayUser({ name, email, age }) {
console.log(`${name} (${age}) - ${email}`);
}
const userData = { name: "Alice", email: "alice@example.com", age: 28 };
displayUser(userData);
Spread and Rest Operators: The Three Dots That Do Everything
The ... syntax serves two purposes depending on where you use it.
Spread operator expands an array or object into its elements:
const numbers = [1, 2, 3];
const moreNumbers = [...numbers, 4, 5, 6];
// [1, 2, 3, 4, 5, 6]
const user = { name: "Bob", age: 30 };
const updatedUser = { ...user, age: 31 };
// { name: "Bob", age: 31 }
Rest operator collects multiple elements into a single array:
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4); // 10
This is especially handy when you don't know how many arguments a function will receive.
Promises and Async/Await: Taming Asynchronous Code
Callbacks were the standard way to handle async operations, but they led to "callback hell"—nested functions that were hard to read and maintain. Promises provided a cleaner approach.
// A simple promise
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
fetchData.then(data => console.log(data));
But the real game-changer came with async/await in ES2017. It makes asynchronous code look synchronous:
async function getUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
}
}
No more nested .then() chains. Just clean, readable code that flows from top to bottom.
Arrow Functions and Lexical this
We touched on arrow functions earlier, but the this behavior deserves its own spotlight. In regular functions, this depends on how the function is called. In arrow functions, this is taken from the surrounding scope.
This is a lifesaver in event handlers and callbacks:
class Button {
constructor() {
this.clickCount = 0;
}
// Old way - need to bind this
handleClickOld() {
this.clickCount++;
}
// ES6 way - arrow function captures this
handleClick = () => {
this.clickCount++;
}
}
Default Parameters: No More Undefined Checks
How many times have you written code like this?
function greet(name) {
name = name || "Guest";
return `Hello, ${name}!`;
}
ES6 lets you set default values directly in the function signature:
function greet(name = "Guest") {
return `Hello, ${name}!`;
}
greet(); // "Hello, Guest!"
greet("PythonSkillset"); // "Hello, PythonSkillset!"
You can even use previous parameters as defaults for later ones:
function createUser(name, role = "user", isActive = true) {
return { name, role, isActive };
}
Classes: Syntactic Sugar Over Prototypes
JavaScript has always had prototypal inheritance, but it wasn't always intuitive. ES6 classes provide a cleaner syntax that looks familiar to developers from class-based languages.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
const rex = new Dog("Rex");
rex.speak(); // "Rex barks."
Under the hood, it's still prototypal inheritance. But the syntax is much more approachable.
Modules: Organize Your Code Properly
Before ES6, JavaScript had no built-in module system. Developers used script tags, IIFEs, or third-party libraries like RequireJS. ES6 modules changed that.
// math.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
// app.js
import { add, PI } from './math.js';
console.log(add(2, 3)); // 5
You can also use default exports:
// utils.js
export default function formatDate(date) {
return date.toISOString().split('T')[0];
}
// app.js
import formatDate from './utils.js';
This makes code organization much cleaner. No more global namespace pollution.
Promises: The Foundation of Modern Async
Promises represent a value that might be available now, later, or never. They have three states: pending, fulfilled, or rejected.
const fetchUser = new Promise((resolve, reject) => {
const user = { id: 1, name: "Alice" };
// Simulate async operation
setTimeout(() => {
resolve(user);
// or reject(new Error("User not found"));
}, 1000);
});
fetchUser
.then(user => console.log(user.name))
.catch(error => console.error(error));
Chaining promises is much cleaner than nested callbacks:
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => displayPosts(posts))
.catch(error => handleError(error));
Template Literals: More Than Just String Interpolation
We already covered basic template literals, but they can do more. You can use them for tagged templates, which let you process template literals with a function.
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return `${result}${str}<strong>${values[i] || ''}</strong>`;
}, '');
}
const name = "PythonSkillset";
const result = highlight`Welcome to ${name}!`;
// "Welcome to <strong>PythonSkillset</strong>!"
This is useful for sanitizing user input or creating custom formatting.
Destructuring: Beyond the Basics
We saw simple destructuring, but it gets more powerful. You can rename variables, set defaults, and destructure nested objects.
const user = {
id: 42,
name: "Bob",
address: {
city: "New York",
zip: "10001"
}
};
// Rename and set defaults
const { name: userName, age = 25, address: { city } } = user;
console.log(userName); // "Bob"
console.log(city); // "New York"
This is incredibly useful when working with API responses that have deeply nested data.
Modules: The Right Way to Share Code
ES6 modules are now supported natively in modern browsers and Node.js. Each file is its own module, and you explicitly export what you want to share.
// utils/helpers.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const VERSION = "1.0.0";
// app.js
import { capitalize, VERSION } from './utils/helpers.js';
console.log(capitalize("pythonSkillset")); // "PythonSkillset"
You can also import everything as a namespace:
import * as Helpers from './utils/helpers.js';
Helpers.capitalize("hello");
Promises: The Foundation of Modern Async
We already covered the basics, but let's look at a real-world example. Imagine you're building a weather app that needs to fetch data from an API.
function getWeather(city) {
return fetch(`https://api.weather.com/${city}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
}
getWeather("London")
.then(data => console.log(data.temperature))
.catch(error => console.error("Weather fetch failed:", error));
With async/await, this becomes even cleaner:
async function displayWeather(city) {
try {
const data = await getWeather(city);
console.log(`Temperature in ${city}: ${data.temperature}°C`);
} catch (error) {
console.error("Couldn't get weather:", error);
}
}
Modules: The Backbone of Modern JavaScript Apps
ES6 modules are now the standard way to organize JavaScript code. They're supported in all modern browsers and Node.js (with "type": "module" in package.json).
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.js
import { add, subtract } from './math.js';
console.log(add(5, 3)); // 8
You can also rename imports:
import { add as sum } from './math.js';
console.log(sum(2, 2)); // 4
Practical Example: Building a Simple Todo App
Let's put everything together. Here's a small todo app that uses ES6 features:
// todo.js
export class TodoList {
constructor() {
this.tasks = [];
}
addTask(title) {
const task = {
id: Date.now(),
title,
completed: false
};
this.tasks = [...this.tasks, task];
return task;
}
completeTask(id) {
this.tasks = this.tasks.map(task =>
task.id === id ? { ...task, completed: true } : task
);
}
getPendingTasks() {
return this.tasks.filter(task => !task.completed);
}
}
// app.js
import { TodoList } from './todo.js';
const myList = new TodoList();
myList.addTask("Learn ES6 features");
myList.addTask("Build a project");
myList.completeTask(myList.tasks[0].id);
console.log(myList.getPendingTasks());
// [{ id: ..., title: "Build a project", completed: false }]
Why This Matters for Your Career
Modern JavaScript isn't just about writing less code—it's about writing code that's easier to read, maintain, and debug. Companies like PythonSkillset see developers who master ES6 features as more productive and better team players.
The transition from ES5 to ES6 might feel overwhelming at first, but start with one feature at a time. Use let and const today. Try arrow functions tomorrow. Before you know it, you'll be writing cleaner, more expressive JavaScript that's a joy to work with.
Remember: every expert was once a beginner. The key is to practice these features in real projects, not just tutorials. Open your code editor, create a small project, and start using ES6 syntax. You'll be amazed at how quickly it becomes second nature.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.