Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected
Tech

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.

July 2026 12 min read 1 views 0 hearts

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.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.