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.
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
- Use
const
by default,let
when reassignment is needed - Prefer arrow functions for callbacks and functional programming
- Use destructuring to extract values clearly
- Template literals for string concatenation
- Default parameters instead of conditional checks
- Spread operator for array/object operations
- Classes for object-oriented programming
- Modules for code organization
Map
/Set
for collections instead of plain objects- 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
andconst
- 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!