A Detailed Guide on Modern JavaScript ES6+ Features

A Detailed Guide on Modern JavaScript ES6+ Features


11 min read

Since 1995, when Brendan Eich wrote the scripting language now known as JavaScript, JavaScript has undergone remarkable updates in recent years. Every year, the ECMAScript Foundation runs a JavaScript update and releases new features for JavaScript. The first update (ES1) was released in 1997, followed by other notable releases.

These updates have made JavaScript a dynamic programming language with a plethora of modern features and syntax to help you write less code, improve your code's readability, and generally do more with JavaScript.

The purpose of this handbook is to teach you the most popular JavaScript ES6+ features with clear explanations, use cases, and code snippets where necessary. However, to better understand these features, you need a strong grasp of HTML and basic JavaScript concepts.

If you're ready, let's jump in and get started!

Major ES6+ Features In JavaScript

A). let and const declarations

Variable declaration is a common practice in JavaScript as well as in most programming languages. The oldest way to declare a variable in JavaScript was with the use of the var keyword. For instance:

var firstName = "Michael";
console.log(firstName) // Michael

However, variable declarations with the var keyword came with a lot of limitations that led to problems in code readability, and maintainability. Some of the key problems include but are not limited to:

  • Function scope

  • Hoisting

  • Variable redeclaration

To solve these problems, in 2015, the let and const keywords were introduced. The let and const keywords which are block-scoped (that is, they are limited to the block in which they are defined), helped to improve variable management.

When to Use var, let, or const

There are two important concepts to familiarize yourself with before deciding to use either of the keywords: scope and hoisting.

I. Scope

I want you to think of scopes like a set of invisible boxes that hold your variables and functions. Each box is like a separate room, and these rooms can be nested (or put) inside each other rooms. If you're familiar with Russian nesting dolls, you'll understand better.

So, when you create a variable or a function, it's like placing an item inside one of these boxes.

  • Global Scope: The outermost box is called the global scope. Variables and functions in this scope can be accessed from anywhere in your code.

  • Local Scope: Inside the global scope, you can have smaller boxes (local scopes). Variables and functions in local scopes can only be accessed from within the same box.

  • Nested Scope: You can have local scopes inside other local scopes, creating a nesting effect. Variables in an inner scope can access variables in outer scopes, but not the other way around.

II. Hoisting

Imagine that before your code runs, JavaScript moves all variable declarations and function declarations to the top of their respective scopes. This is exactly what happens in hoisting. The variables are moved up to the beginning of the boxes containing them.

  • Variable Hoisting: When you declare a variable with var, JavaScript hoists it to the top of its current scope. However, the actual value you give the variable stays in its original position. This most times leads to unexpected behaviour in the code base.

  • Function Hoisting: Function declarations are also hoisted to the top of their scope. This means you can use a function before you declare it in your code.

Let's look at a simple example:

console.log(myAge); // undefined (variable is hoisted)
var myAge= 42;

sayHi(); // "Hi, Sandra!" (function is hoisted)
function sayHi() {
  console.log("Hi, Sandra!");

In the code above, both the variable myAge and the function sayHi are hoisted to the top of their scope, but the value you assigned to myVar remains in its original place. This is why myAge is undefined when we first console.log it, but sayHi() works just fine.

So, the key takeaway is that scope determines where you can access your variables and functions, while hoisting is like JavaScript secretly moving declarations to the top of their scopes before running your code.

Here's a table summary (by Dillion Megida) showing the differences between these keywords:

KeywordsScopeRedeclaration and ReassignmentHoisting
varGlobal, Localyes and yesYes, with a default value
letGlobal, Local, and blockno and yesYes, without a default value
constGlobal, Local, and blockno & noYes, without a default value

Now, when do you use either of the keywords?

  • Use var only when you intentionally want a variable to be function-scoped and are aware of its potential harm in your program's execution.

  • Use let when you want block-scoped variables that can be reassigned.

  • Use const when you want a variable that cannot be reassigned. This is particularly useful for defining values that should not change during your program's execution.

B). Arrow functions

You're already familiar with regular functions and how to use them. As a reminder, the regular function has the below syntax:

function functionName(parameters) {
  // function body

The arrow function was introduced in ES6 (ECMAScript 2015) to help us write a more concise and readable code.

Look at the syntax:

(parameters) => expression

Apart from the syntax, arrow functions have some key differences from regular function expressions. Let's at a few of the major ones:

  • Parameters:

    • Arrow functions can take zero or more parameters enclosed in parentheses.
  • If there is only one parameter, you can omit the parentheses.
//Arrow function with parameters and a single expression:
const add = (a, b) => a + b;

//Arrow function with a single parameter (no parentheses required):
const square = x => x * x;

//Arrow function with multiple statements (use curly braces and return):
const multiply = (x, y) => {
  const result = x * y;
  return result;
  • Lexical this Binding:

    • Arrow functions do not have their own this context.

    • They inherit the this value from the surrounding (enclosing) function or scope.

    • This behaviour is particularly useful when working with functions inside objects and closures.

To better understand the this keyword, read my post on The Simplified Guide to the 'this' Keyword in JavaScript. You'll learn almost everything about this keyword and how it applies to arrow functions.

  • You can use the Arrow function with map, filter, and reduce built-in functions.

Again, I have written a deep dive on array methods in JavaScript to help you understand this more.

When to Use Arrow Functions

  • Use arrow functions only when you want simple and concise functions, especially when you want to maintain the lexical this context.

  • For complex functions or methods that require their own this context, use regular function expressions.

C). Template Literals

Ever felt frustrated by too much use of \n and the escape character (backslash) \ in your code when trying to write multiline strings in JavaScript? Template literals solve that problem and do even more.

Template literals allow you to include variables, expressions, and even function calls within a string enclosed by backticks (``) in very convenient and readable ways.

You'll find the backticks (``) in the key before number 1 on your keyboard (depending on the PC you use). But this is common in most PCs.

Let's look at template literal syntax and some use cases.

const greetings = `Hello, ${name}!`;

One primary use case of template literals (often referred to as embedded expression) is to interpolate variables into strings.

Consider the example below:

const name = "Michael";
const greeting = `Hi, ${name}!`;
console.log(greeting); // Outputs: "Hi, Michael!"

In this example, ${name} is an embedded expression that inserts the value of the name variable into the string. Notice you didn't have to use the plus (+) operator to concatenate the strings.

Awesome, right?

In addition, you can even call functions and include their return values within embedded expressions:

function getMyAge() {
  return 28;

const myAge = `My age is ${getMyAge()} years.`;
console.log(myAge); // Outputs: "My age is 30 years."

In this case, ${getAge()} calls the getAge function and inserts its return value into the string.

Template literals are also helpful in creating multiline strings with embedded expressions. This makes it easy to format complex text.

Take a look at the example below:

const name = "Luke";
const message = `
  Hello ${name},

  We are glad to inform you've been selected for the scholarship.

  Your Team

This is especially useful when you want to add a placeholder for whatever you want.

D). Array and Object Destructuring

Destructuring provides an easier and more concise way to extract values from arrays or properties from objects into distinct variables.

If you have read my post on deep dive into array methods in JavaScript, you already how to retrieve or access elements from arrays. Destructuring makes the whole process better and concise.

Let's look at both approaches:

Array Indexing (Old Method):

const myFriendsAge = [28, 35, 23];

const first = myFriendsAge [0];
const second= myFriendsAge [1];
const third = myFriendsAge [2];

console.log(first , second, third); // Outputs: 28 35 23

Array Destructuring:

const myFriendsAge = [28, 35, 23];

const [first, second, third] = myFriendsAge;
console.log(first , second, third); // Outputs: 28 35 23

The same thing applies to objects. Here's an example:

Object Dot Notation (Old Way):

const friend = { name: "Luke", age: 30 };
const name = friend.name;
const age = friend.age;
console.log(name, age); // Outputs: "Luke" 30

Object Destructuring:

const person = { name: "Alice", age: 30 };
const { name, age } = person;
console.log(name, age); // Outputs: "Alice" 30

With object destructuring, you eliminate the need for repetitive dot notation and simplify property extraction from objects.

Object destructuring equally comes in handy when you're working with nested objects.

Let me show you an example.

The Old Way (Nested Dot Notation):

const nestedData = { 
  friend: { 
    name: "Gideon", 
    email: "gideon@softwareengineer.com" 
const name = nestedData.friend.name;
const email = nestedData.friend.email;
console.log(name, email); // Outputs: "Gideon" "gideon@softwareengineer.com"

Nested Destructuring:

const nestedData = { 
  user: { 
    name: "Alice", 
    email: "alice@example.com" 
const { user: { name, email } } = nestedData;
console.log(name, email); // Outputs: "Alice" "alice@example.com"

E). Default Parameters

Default parameters simply allow you to specify default values for function parameters. This simple feature helps to enhance the flexibility and readability of your functions by providing a better way to handle missing or undefined arguments.

Here's the basic syntax:

function functionName(param1 = defaultValue1, param2 = defaultValue2, ...) {
  // Function body

The defaultValue can be any valid JavaScript expression, including literals, variables, or function calls. The default value will be used if you omit a parameter when calling the function or explicitly pass it as undefined.

Let's look at a few use cases.

  • Default parameters help prevent errors caused by undefined values. Consider this example:
function greeting(name) {
  console.log(`Howdy, ${name}!`);

greet(); // Outputs: "Howdy, undefined!"

If you call greeting() without providing an argument, it results in "undefined" being used as name. However, by using default parameters, you can improve the function as thus:

function greeting(name = 'stranger') {
  console.log(`Hello, ${name}!`);

greeting(); // Outputs: "Hello, stranger!"

So now, if no name is provided, it defaults to "stranger."

  • Default parameters make it easy to create flexible functions. For example, you can create a function that calculates the perimeter of a rectangle with customizable default values for its dimensions.

    Here's an example:

function calculateRectanglePerimeter(length= 1, height = 1) {
  return 2(length + height);

const area1 = calculateRectanglePerimeter(); // Defaults to 2(1+1) rectangle
const area2 = calculateRectanglePerimeter(5, 10); // Calculates perimeter of a 2(5+10) rectangle

This function allows you to quickly compute rectangle perimeters with different dimensions while providing sensible defaults.

  • One final use case of the default parameters may be in handling optional callbacks. When you're working with asynchronous operations or event handlers that accept callback functions, default parameters can be used to provide a no-op function as the default callback. This prevents errors when the callback is not provided.

Look at a good example:

function fetchData(url, callback = () => {}) {
  // Fetch data from 'url' and invoke 'callback'

fetchData('https://example.com'); // No callback provided, but no error occurs

In the above example, if no callback is passed, an empty arrow function acts as a safe default and that's really cool.

F). Object Literal

ES6 allows you to define object properties and methods in a more concise way when their names match the variable names you want to use. This shorthand notation significantly reduces redundancy in your code.

Look at an example:

// In JavaScript ES5
var name = 'John';
var age = 30;
var person = {
  name: name,
  age: age,
  greet: function() {
    console.log('Hello, ' + this.name + '!');

// In JavaScript ES6
const name = 'John';
const age = 30;
const person = {
  greet() {
    console.log(`Hello, ${this.name}!`);


This first part of the guide has you into modern JavaScript ES6+ features. We've specifically focused on 'let' and 'const' for variable declaration, arrow functions for concise code, and template literals for versatile string manipulation. We explored array and object destructuring, default parameters and object literals.

These concepts will help you you write more organized and maintainable code, fostering best practices in your JavaScript projects.

In this part, we'll explore the remaining JavaScript ES6+ features and also include best practices to for efficiency and maintainability.