ES6+ FeaturesFeatured

ES6 Features: Modern JavaScript Complete Guide

Master all ES6 (ECMAScript 2015) features that revolutionized JavaScript. Learn arrow functions, destructuring, modules, classes, and more with practical examples.

By JavaScriptDoc Team
es6ecmascriptmodern javascriptarrow functionsdestructuringmodules

ES6 Features: Modern JavaScript Complete Guide

ES6 (ECMAScript 2015) brought the most significant update to JavaScript, introducing features that made the language more powerful and developer-friendly. This guide covers all major ES6 features with practical examples.

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;

// Single parameter (parentheses optional)
const square = (x) => x * x;

// No parameters
const getRandom = () => Math.random();

// Multiple statements (need curly braces and return)
const greet = (name) => {
  const message = `Hello, ${name}!`;
  console.log(message);
  return message;
};

// Returning object literals (need parentheses)
const createUser = (name, age) => ({ name, age });

Lexical this Binding

// Traditional function - 'this' problem
const timer = {
  seconds: 0,
  start: function () {
    setInterval(function () {
      this.seconds++; // 'this' is undefined or window
    }, 1000);
  },
};

// Arrow function solution
const timer = {
  seconds: 0,
  start: function () {
    setInterval(() => {
      this.seconds++; // 'this' refers to timer object
    }, 1000);
  },
};

// When NOT to use arrow functions
const obj = {
  name: 'Object',

  // Bad - arrow function doesn't have its own 'this'
  greet: () => {
    console.log(`Hello, ${this.name}`); // undefined
  },

  // Good - regular method
  greet() {
    console.log(`Hello, ${this.name}`); // 'Object'
  },
};

Let and Const

New ways to declare variables with block scope.

// var - function scoped, hoisted
function varExample() {
  console.log(x); // undefined (hoisted)
  var x = 1;

  if (true) {
    var y = 2; // Function scoped
  }

  console.log(y); // 2 (accessible)
}

// let - block scoped
function letExample() {
  // console.log(x); // ReferenceError (temporal dead zone)
  let x = 1;

  if (true) {
    let y = 2; // Block scoped
    let x = 3; // Different variable
    console.log(x); // 3
  }

  console.log(x); // 1
  // console.log(y); // ReferenceError
}

// const - block scoped, cannot be reassigned
const PI = 3.14159;
// PI = 3.14; // TypeError

// But objects/arrays can be mutated
const user = { name: 'John' };
user.name = 'Jane'; // OK
user.age = 30; // OK
// user = {}; // TypeError

// Prevent mutations
const frozenUser = Object.freeze({ name: 'John' });
// frozenUser.name = 'Jane'; // Silently fails (strict mode throws)

Template Literals

Multi-line strings and string interpolation.

// Basic usage
const name = 'John';
const age = 30;

// Old way
const message1 = 'Hello, ' + name + '! You are ' + age + ' years old.';

// Template literal
const message2 = `Hello, ${name}! You are ${age} years old.`;

// Multi-line strings
const multiline = `
  This is a
  multi-line
  string
`;

// Expression interpolation
const price = 19.99;
const quantity = 3;
console.log(`Total: $${(price * quantity).toFixed(2)}`); // Total: $59.97

// Nested templates
const isVIP = true;
const greeting = `Welcome ${name}${isVIP ? `, our ${`valued`} customer` : ''}!`;

// Tagged templates
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '';
    return result + str + value;
  }, '');
}

const highlighted = highlight`Hello ${name}, you are ${age} years old`;
// "Hello <mark>John</mark>, you are <mark>30</mark> years old"

// Raw strings
String.raw`Line 1\nLine 2`; // "Line 1\\nLine 2" (backslash not escaped)

Destructuring

Extract values from arrays and objects into distinct variables.

Array Destructuring

// Basic array destructuring
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3

// Skipping elements
const [first, , third] = [1, 2, 3];
console.log(first, third); // 1 3

// Rest elements
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

// Default values
const [x = 0, y = 0] = [1];
console.log(x, y); // 1 0

// Swapping variables
let p = 1,
  q = 2;
[p, q] = [q, p];
console.log(p, q); // 2 1

// Nested destructuring
const nested = [1, [2, 3], 4];
const [one, [two, three], four] = nested;

// Function returns
function getCoordinates() {
  return [10, 20];
}
const [x1, y1] = getCoordinates();

Object Destructuring

// Basic object destructuring
const user = { name: 'John', age: 30, email: 'john@example.com' };
const { name, age } = user;
console.log(name, age); // John 30

// Different variable names
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // John 30

// Default values
const { phone = 'N/A' } = user;
console.log(phone); // N/A

// Rest properties
const { name: n, ...rest } = user;
console.log(n); // John
console.log(rest); // { age: 30, email: 'john@example.com' }

// Nested destructuring
const company = {
  name: 'TechCorp',
  address: {
    street: '123 Main St',
    city: 'New York',
    country: 'USA',
  },
};

const {
  address: { city, country },
} = company;
console.log(city, country); // New York USA

// Function parameters
function greet({ name, age = 18 }) {
  return `Hello ${name}, you are ${age} years old`;
}

greet({ name: 'John', age: 30 }); // Hello John, you are 30 years old

// Complex example
const users = [
  { id: 1, name: 'John', skills: ['JS', 'React'] },
  { id: 2, name: 'Jane', skills: ['Python', 'Django'] },
];

for (const {
  name,
  skills: [primary],
} of users) {
  console.log(`${name}'s primary skill: ${primary}`);
}

Default Parameters

Function parameters with default values.

// Basic default parameters
function greet(name = 'Guest', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}

console.log(greet()); // Hello, Guest!
console.log(greet('John')); // Hello, John!
console.log(greet('John', 'Hi')); // Hi, John!

// Default parameters can reference other parameters
function createUser(
  name,
  role = 'user',
  permissions = role === 'admin' ? ['all'] : ['read']
) {
  return { name, role, permissions };
}

// Default parameters are evaluated at call time
let value = 0;
function getValue() {
  return ++value;
}

function test(x = getValue()) {
  return x;
}

console.log(test()); // 1
console.log(test()); // 2
console.log(test(10)); // 10

// Destructuring with defaults
function processUser({ name = 'Anonymous', age = 0 } = {}) {
  console.log(`${name} is ${age} years old`);
}

processUser(); // Anonymous is 0 years old
processUser({ name: 'John' }); // John is 0 years old

Rest and Spread Operators

Three dots (...) with different behaviors.

Rest Parameters

// Collect remaining arguments
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// Rest must be last parameter
function introduce(greeting, ...names) {
  return `${greeting} ${names.join(' and ')}!`;
}

console.log(introduce('Hello', 'John', 'Jane', 'Bob')); // Hello John and Jane and Bob!

// Replacing arguments object
function oldWay() {
  console.log(arguments); // array-like object
}

function newWay(...args) {
  console.log(args); // real array
  args.forEach((arg) => console.log(arg));
}

Spread Operator

// Spread in arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// Concatenating arrays
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Copying arrays
const copy = [...arr1]; // [1, 2, 3]

// Convert array-like to array
const nodeList = document.querySelectorAll('div');
const divArray = [...nodeList];

// Function calls
const numbers = [1, 2, 3];
console.log(Math.max(...numbers)); // 3

// Spread in objects (ES2018)
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

// Merging objects
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }

// Copying objects (shallow)
const objCopy = { ...obj1 }; // { a: 1, b: 2 }

// Overriding properties
const updated = { ...obj1, b: 3, e: 5 }; // { a: 1, b: 3, e: 5 }

Classes

Syntactic sugar over JavaScript's prototype-based inheritance.

// Class declaration
class Person {
  // Constructor
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // Instance method
  greet() {
    return `Hello, I'm ${this.name}`;
  }

  // Getter
  get info() {
    return `${this.name} is ${this.age} years old`;
  }

  // Setter
  set info(value) {
    const [name, age] = value.split(',');
    this.name = name;
    this.age = parseInt(age);
  }

  // Static method
  static createGuest() {
    return new Person('Guest', 0);
  }
}

// Using the class
const john = new Person('John', 30);
console.log(john.greet()); // Hello, I'm John
console.log(john.info); // John is 30 years old

const guest = Person.createGuest();

// Class expression
const Animal = class {
  constructor(name) {
    this.name = name;
  }
};

// Inheritance
class Employee extends Person {
  constructor(name, age, role) {
    super(name, age); // Call parent constructor
    this.role = role;
  }

  // Override parent method
  greet() {
    return `${super.greet()}, I work as ${this.role}`;
  }

  // New method
  work() {
    return `${this.name} is working`;
  }
}

const jane = new Employee('Jane', 25, 'Developer');
console.log(jane.greet()); // Hello, I'm Jane, I work as Developer
console.log(jane instanceof Employee); // true
console.log(jane instanceof Person); // true

// Private fields (ES2022)
class BankAccount {
  #balance = 0; // Private field

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    this.#balance += amount;
  }

  get balance() {
    return this.#balance;
  }
}

Modules

Native module system for JavaScript.

Named Exports

// math.js
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// Alternative syntax
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;

export { subtract, divide };

// main.js
import { add, multiply, PI } from './math.js';

console.log(add(2, 3)); // 5
console.log(multiply(3, 4)); // 12
console.log(PI); // 3.14159

// Import all as namespace
import * as math from './math.js';

console.log(math.add(2, 3)); // 5
console.log(math.PI); // 3.14159

// Rename imports
import { add as sum, multiply as mult } from './math.js';

Default Exports

// user.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}

// Or
class User {
  constructor(name) {
    this.name = name;
  }
}
export default User;

// main.js
import User from './user.js';

const john = new User('John');

// Can combine default and named exports
// utils.js
export default function process(data) {
  return data.toUpperCase();
}

export const VERSION = '1.0.0';
export const API_URL = 'https://api.example.com';

// main.js
import process, { VERSION, API_URL } from './utils.js';

Dynamic Imports

// Static imports are hoisted and synchronous
import { utils } from './utils.js';

// Dynamic imports return promises
async function loadModule() {
  const module = await import('./math.js');
  console.log(module.add(2, 3));
}

// Conditional imports
if (condition) {
  import('./feature.js').then((module) => {
    module.enableFeature();
  });
}

// Lazy loading
button.addEventListener('click', async () => {
  const { handleClick } = await import('./handlers.js');
  handleClick();
});

Enhanced Object Literals

Shorter syntax for object properties and methods.

// Property shorthand
const name = 'John';
const age = 30;

// Old way
const user1 = {
  name: name,
  age: age,
};

// ES6 way
const user2 = {
  name,
  age,
};

// Method shorthand
const calculator = {
  // Old way
  add: function (a, b) {
    return a + b;
  },

  // ES6 way
  subtract(a, b) {
    return a - b;
  },
};

// Computed property names
const prop = 'name';
const obj = {
  [prop]: 'John',
  ['user' + 'Id']: 123,
  [Symbol.for('id')]: 'symbol-value',
};

console.log(obj.name); // John
console.log(obj.userId); // 123

// Dynamic method names
const methodName = 'greet';
const greeter = {
  [methodName]() {
    return 'Hello!';
  },
  [`${methodName}Loudly`]() {
    return 'HELLO!';
  },
};

console.log(greeter.greet()); // Hello!
console.log(greeter.greetLoudly()); // HELLO!

Symbols

A new primitive type for unique identifiers.

// Creating symbols
const sym1 = Symbol();
const sym2 = Symbol('description');
const sym3 = Symbol('description');

console.log(sym2 === sym3); // false (always unique)

// Using symbols as object properties
const id = Symbol('id');
const user = {
  name: 'John',
  [id]: 123,
};

console.log(user[id]); // 123
console.log(Object.keys(user)); // ['name'] - symbols not enumerated

// Well-known symbols
const iterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;

    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        }
        return { done: true };
      },
    };
  },
};

for (const value of iterable) {
  console.log(value); // 1, 2, 3
}

// Global symbol registry
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');

console.log(globalSym1 === globalSym2); // true

// Get key for global symbol
console.log(Symbol.keyFor(globalSym1)); // 'app.id'

Iterators and Generators

Custom iteration behavior and function execution control.

Iterators

// Custom iterator
const range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;

    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { done: true };
      },
    };
  },
};

for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// Using iterators directly
const iterator = range[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }

Generators

// Generator function
function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

// Generator with parameters
function* fibonacci(n) {
  let a = 0,
    b = 1;

  for (let i = 0; i < n; i++) {
    yield a;
    [a, b] = [b, a + b];
  }
}

for (const num of fibonacci(10)) {
  console.log(num); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

// Infinite generator
function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

// Two-way communication
function* twoWay() {
  const input = yield 'First output';
  yield `You said: ${input}`;
}

const gen2 = twoWay();
console.log(gen2.next()); // { value: 'First output', done: false }
console.log(gen2.next('Hello')); // { value: 'You said: Hello', done: false }

// Delegating generators
function* gen1() {
  yield 1;
  yield 2;
}

function* gen2() {
  yield* gen1();
  yield 3;
}

for (const value of gen2()) {
  console.log(value); // 1, 2, 3
}

Map and Set

New collection types with better performance and features.

Map

// Creating maps
const map = new Map();

// Setting values
map.set('name', 'John');
map.set(42, 'number key');
map.set(true, 'boolean key');

const objKey = { id: 1 };
map.set(objKey, 'object key');

// Getting values
console.log(map.get('name')); // John
console.log(map.get(42)); // number key
console.log(map.get(objKey)); // object key

// Map operations
console.log(map.has('name')); // true
console.log(map.size); // 4

map.delete(42);
console.log(map.size); // 3

// Iteration
for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}

// Convert to array
const entries = [...map]; // Array of [key, value] pairs
const keys = [...map.keys()];
const values = [...map.values()];

// Initialize with data
const map2 = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

// Object vs Map
// - Maps can use any value as key
// - Maps maintain insertion order
// - Maps have size property
// - Maps are iterable
// - Better performance for frequent additions/deletions

Set

// Creating sets
const set = new Set();

// Adding values
set.add(1);
set.add(2);
set.add(3);
set.add(2); // Duplicate, ignored

console.log(set.size); // 3

// Set operations
console.log(set.has(2)); // true
set.delete(2);
console.log(set.has(2)); // false

// Initialize with array
const set2 = new Set([1, 2, 3, 3, 4, 4, 5]);
console.log(set2.size); // 5 (duplicates removed)

// Convert to array
const uniqueArray = [...set2]; // [1, 2, 3, 4, 5]

// Remove duplicates from array
const numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5];
const unique = [...new Set(numbers)]; // [1, 2, 3, 4, 5]

// Set operations
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);

// Union
const union = new Set([...setA, ...setB]); // {1, 2, 3, 4, 5}

// Intersection
const intersection = new Set([...setA].filter((x) => setB.has(x))); // {3}

// Difference
const difference = new Set([...setA].filter((x) => !setB.has(x))); // {1, 2}

WeakMap and WeakSet

Collections with weak references to objects.

// WeakMap - keys must be objects
const weakMap = new WeakMap();

const obj1 = { name: 'John' };
const obj2 = { name: 'Jane' };

weakMap.set(obj1, 'data for John');
weakMap.set(obj2, 'data for Jane');

console.log(weakMap.get(obj1)); // data for John

// When obj1 is garbage collected, its entry is removed
// obj1 = null; // Entry will be removed

// Use case: private data
const privateData = new WeakMap();

class User {
  constructor(name) {
    privateData.set(this, { name });
  }

  getName() {
    return privateData.get(this).name;
  }
}

// WeakSet - values must be objects
const weakSet = new WeakSet();

const obj3 = { id: 1 };
const obj4 = { id: 2 };

weakSet.add(obj3);
weakSet.add(obj4);

console.log(weakSet.has(obj3)); // true

// Use case: tracking objects without preventing GC
const visitedNodes = new WeakSet();

function processNode(node) {
  if (visitedNodes.has(node)) {
    return; // Already processed
  }

  visitedNodes.add(node);
  // Process node...
}

Promise (Covered in detail in separate guide)

// Basic Promise usage
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

promise
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

// Promise methods
Promise.all([promise1, promise2, promise3]).then((results) =>
  console.log(results)
);

Promise.race([promise1, promise2, promise3]).then((winner) =>
  console.log(winner)
);

Proxy and Reflect

Intercept and customize object operations.

// Basic Proxy
const target = {
  name: 'John',
  age: 30,
};

const handler = {
  get(target, property) {
    console.log(`Getting ${property}`);
    return target[property];
  },

  set(target, property, value) {
    console.log(`Setting ${property} to ${value}`);
    target[property] = value;
    return true;
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Getting name → John
proxy.age = 31; // Setting age to 31

// Validation proxy
const user = new Proxy(
  {},
  {
    set(target, property, value) {
      if (property === 'age' && typeof value !== 'number') {
        throw new TypeError('Age must be a number');
      }

      target[property] = value;
      return true;
    },
  }
);

// Reflect API
const obj = { a: 1 };

Reflect.set(obj, 'b', 2);
console.log(Reflect.get(obj, 'b')); // 2
console.log(Reflect.has(obj, 'a')); // true
Reflect.deleteProperty(obj, 'a');

Best Practices

  1. Use const by default, let when reassignment is needed
  2. Prefer arrow functions for callbacks and functional programming
  3. Use destructuring to extract values clearly
  4. Template literals for string concatenation
  5. Default parameters instead of conditional checks
  6. Spread operator for array/object operations
  7. Classes for object-oriented programming
  8. Modules for code organization
  9. Map/Set for collections instead of plain objects
  10. Async/await over Promise chains (ES2017)

Conclusion

ES6 transformed JavaScript into a more powerful and expressive language. Key features to master:

  • Arrow functions and lexical this
  • Block-scoped variables with let and const
  • Destructuring for cleaner code
  • Template literals for string handling
  • Classes for OOP
  • Modules for better code organization
  • New data structures like Map and Set
  • Promises for asynchronous programming

These features are now essential for modern JavaScript development. Practice using them to write cleaner, more maintainable code!