Modern JavaScript for Developers in 2026
The Complete Guide to ES6+ JavaScript Features: Modern JavaScript for Developers in 2026
Master the essential JavaScript features that every developer needs to know
JavaScript has come a long way since its early days. The introduction of ES6 (ECMAScript 2015) marked a turning point in web development, bringing powerful features that make code cleaner, more readable, and easier to maintain. Whether you're a beginner looking to understand modern JavaScript or an experienced developer wanting to brush up on the latest features, this comprehensive guide covers everything you need to know.
Understanding ES6 and Why It Matters
ES6, officially known as ECMAScript 2015, was released in June 2015 after six years of development. It introduced dozens of new features that addressed long-standing issues in JavaScript and brought the language in line with modern programming practices.
Before ES6, JavaScript developers had to work around many limitations and quirks in the language. Variables had confusing scope rules, functions were verbose, and handling asynchronous operations often led to "callback hell." ES6 solved these problems and more, making JavaScript a more powerful and enjoyable language to work with.
Why should you care about ES6+?
- Better code quality: Features like
let
/const
and arrow functions reduce bugs and improve readability - Industry standard: Modern frameworks like React, Vue, and Angular rely heavily on ES6+ features
- Career advancement: Most job interviews for JavaScript positions expect knowledge of modern syntax
- Developer productivity: New features significantly reduce the amount of code you need to write
Block Scope with let and const
One of the most significant improvements in ES6 was the introduction of block-scoped variables. Before ES6, JavaScript only had function scope with var
, which led to many confusing bugs and unexpected behaviors.
The Problem with var
function example() {
console.log(x); // undefined (not an error!)
if (true) {
var x = 1;
}
console.log(x); // 1
}
// The above code is equivalent to:
function example() {
var x; // hoisted to the top
console.log(x); // undefined
if (true) {
x = 1;
}
console.log(x); // 1
}
This behavior, called "hoisting," often caught developers off guard and led to bugs.
Enter let: Block-Scoped Variables
The let
keyword creates variables that are limited to the block they're declared in:
function betterExample() {
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
if (true) {
let y = 1;
console.log(y); // 1
}
// console.log(y); // ReferenceError: y is not defined
}
Here's a practical example that shows the difference:
// Using var (problematic)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Prints: 3, 3, 3
}
// Using let (correct behavior)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Prints: 0, 1, 2
}
const: Immutable Variables
The const
keyword creates constants that cannot be reassigned:
const PI = 3.14159;
const user = { name: "Sarah", age: 28 };
// PI = 3.14; // TypeError: Assignment to constant variable.
// However, you can modify object properties:
user.age = 29; // This works!
user.city = "New York"; // This also works!
console.log(user); // { name: "Sarah", age: 29, city: "New York" }
When to use each:
- Use
const
by default - Use
let
when you need to reassign the variable - Avoid
var
in modern JavaScript
Arrow Functions: A New Way to Write Functions
Arrow functions provide a more concise syntax for writing functions and solve the common problem of this
binding in JavaScript.
Basic Syntax
// Traditional function
function greet(name) {
return "Hello, " + name + "!";
}
// Arrow function
const greet = (name) => {
return "Hello, " + name + "!";
};
// Even shorter (implicit return)
const greet = name => `Hello, ${name}!`;
// Multiple parameters
const add = (a, b) => a + b;
// No parameters
const sayHello = () => "Hello, World!";
Lexical this Binding
One of the biggest advantages of arrow functions is that they don't have their own this
context. They inherit this
from the surrounding scope:
class TodoList {
constructor() {
this.todos = [];
this.id = 1;
}
addTodo(text) {
const todo = {
id: this.id++,
text: text,
completed: false
};
this.todos.push(todo);
// Traditional function would lose 'this' context
setTimeout(function() {
console.log(this); // undefined or window object
}, 1000);
// Arrow function preserves 'this' context
setTimeout(() => {
console.log(this.todos); // Correctly refers to TodoList instance
}, 1000);
}
}
Real-World Example: Event Handling
class ButtonHandler {
constructor(element) {
this.element = element;
this.clickCount = 0;
// Arrow function preserves 'this'
this.element.addEventListener('click', () => {
this.clickCount++;
this.element.textContent = `Clicked ${this.clickCount} times`;
});
}
}
// Usage
const button = document.getElementById('my-button');
new ButtonHandler(button);
When NOT to Use Arrow Functions
Arrow functions aren't always the right choice:
// Don't use for object methods
const person = {
name: "John",
greet: () => {
console.log(`Hello, I'm ${this.name}`); // 'this' doesn't refer to person
}
};
// Use regular function instead
const person = {
name: "John",
greet() {
console.log(`Hello, I'm ${this.name}`); // Correctly refers to person
}
};
Template Literals: Better String Handling
Template literals revolutionized string handling in JavaScript, providing string interpolation and multiline strings using backticks (`
).
String Interpolation
const name = "Alice";
const age = 30;
const city = "San Francisco";
// Old way (concatenation)
const message1 = "Hi, I'm " + name + ". I'm " + age + " years old and I live in " + city + ".";
// ES6 way (template literal)
const message2 = `Hi, I'm ${name}. I'm ${age} years old and I live in ${city}.`;
// You can include any JavaScript expression
const message3 = `Next year, I'll be ${age + 1} years old.`;
Multiline Strings
// Old way (ugly and error-prone)
const html1 = "<div class=\"user-card\">\n" +
" <h2>" + user.name + "</h2>\n" +
" <p>Email: " + user.email + "</p>\n" +
"</div>";
// ES6 way (clean and readable)
const html2 = `
<div class="user-card">
<h2>${user.name}</h2>
<p>Email: ${user.email}</p>
<p>Joined: ${new Date(user.joinDate).toLocaleDateString()}</p>
</div>
`;
Advanced Template Literals
You can use any JavaScript expression inside ${}
:
const products = [
{ name: "Laptop", price: 999 },
{ name: "Mouse", price: 25 },
{ name: "Keyboard", price: 75 }
];
const total = products.reduce((sum, product) => sum + product.price, 0);
const receipt = `
Order Summary:
${products.map(product => ` • ${product.name}: $${product.price}`).join('\n')}
Total: $${total}
Tax: $${(total * 0.08).toFixed(2)}
Final Total: $${(total * 1.08).toFixed(2)}
`;
console.log(receipt);
Tagged Template Literals
For advanced use cases, you can create tagged template literals:
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : '';
return result + string + value;
}, '');
}
const searchTerm = "JavaScript";
const text = highlight`Learn ${searchTerm} programming with our comprehensive guide to ${searchTerm} ES6+ features.`;
console.log(text);
// Output: "Learn <mark>JavaScript</mark> programming with our comprehensive guide to <mark>JavaScript</mark> ES6+ features."
Destructuring: Extract Values Like a Pro
Destructuring assignment allows you to extract values from arrays and objects into distinct variables using a syntax that mirrors the construction of array and object literals.
Array Destructuring
const coordinates = [40.7128, -74.0060];
// Old way
const latitude = coordinates[0];
const longitude = coordinates[1];
// ES6 destructuring
const [lat, lng] = coordinates;
console.log(lat, lng); // 40.7128, -74.0060
// Skip elements you don't need
const numbers = [1, 2, 3, 4, 5];
const [first, , third, ...rest] = numbers;
console.log(first, third, rest); // 1, 3, [4, 5]
// Default values
const [x = 10, y = 20, z = 30] = [1, 2];
console.log(x, y, z); // 1, 2, 30
Object Destructuring
const user = {
id: 1,
name: "Emma Watson",
email: "[email protected]",
address: {
street: "123 Main St",
city: "Boston",
state: "MA"
}
};
// Basic destructuring
const { name, email } = user;
console.log(name, email); // "Emma Watson", "[email protected]"
// Rename variables
const { name: userName, email: userEmail } = user;
// Default values
const { name, phone = "Not provided" } = user;
// Nested destructuring
const { name, address: { city, state } } = user;
console.log(city, state); // "Boston", "MA"
// Rest operator with objects
const { name, ...otherProps } = user;
console.log(otherProps); // { id: 1, email: "[email protected]", address: {...} }
Function Parameter Destructuring
Destructuring is especially powerful when used with function parameters:
// API response handler
function handleUserResponse({ data: { user }, status, message }) {
if (status === 'success') {
updateUI(user);
} else {
showError(message);
}
}
// Options pattern
function createChart({
width = 400,
height = 300,
type = 'line',
data,
colors = ['#blue', '#red']
} = {}) {
console.log(`Creating ${type} chart: ${width}x${height}`);
// Chart creation logic here
}
// Usage
createChart({
data: myChartData,
type: 'bar',
colors: ['#green', '#orange']
});
// You can even call it with no arguments thanks to the default empty object
createChart();
Practical Example: React Component Props
// Instead of this:
function UserCard(props) {
return (
<div>
<h2>{props.user.name}</h2>
<p>{props.user.email}</p>
<button onClick={props.onEdit}>Edit</button>
</div>
);
}
// You can write this:
function UserCard({ user: { name, email }, onEdit }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
}
Rest and Spread Operators: The Power of Three Dots {#rest-spread}
The rest and spread operators both use the same syntax (...
) but serve opposite purposes: rest collects elements into an array, while spread expands elements from an array or object.
Spread Operator: Expanding Elements
Array Spreading:
const fruits = ['apple', 'banana'];
const vegetables = ['carrot', 'broccoli'];
// Combining arrays
const food = [...fruits, ...vegetables];
console.log(food); // ['apple', 'banana', 'carrot', 'broccoli']
// Adding elements
const moreFruits = ['orange', ...fruits, 'grape'];
console.log(moreFruits); // ['orange', 'apple', 'banana', 'grape']
// Cloning arrays (shallow copy)
const fruitsCopy = [...fruits];
Object Spreading:
const defaultSettings = {
theme: 'light',
language: 'en',
notifications: true
};
const userSettings = {
theme: 'dark',
fontSize: 16
};
// Merging objects (later properties override earlier ones)
const settings = { ...defaultSettings, ...userSettings };
console.log(settings);
// {
// theme: 'dark', // overridden by userSettings
// language: 'en',
// notifications: true,
// fontSize: 16 // added from userSettings
// }
// Adding properties
const updatedUser = {
...user,
lastLogin: new Date(),
isActive: true
};
Function Arguments:
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6
// Finding max value in array
const scores = [89, 76, 91, 88, 95];
const highestScore = Math.max(...scores);
console.log(highestScore); // 95
Rest Operator: Collecting Elements
Function Parameters:
// Collect all arguments
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10, 20)); // 30
// Mix regular and rest parameters
function introduce(name, age, ...hobbies) {
console.log(`Hi, I'm ${name}, ${age} years old.`);
if (hobbies.length > 0) {
console.log(`I enjoy: ${hobbies.join(', ')}`);
}
}
introduce("Sarah", 28, "reading", "hiking", "photography");
// Hi, I'm Sarah, 28 years old.
// I enjoy: reading, hiking, photography
Array Destructuring:
const [first, second, ...remaining] = [1, 2, 3, 4, 5, 6];
console.log(first); // 1
console.log(second); // 2
console.log(remaining); // [3, 4, 5, 6]
Object Destructuring:
const user = {
id: 1,
name: "John Doe",
email: "[email protected]",
age: 30,
city: "New York"
};
const { name, email, ...otherInfo } = user;
console.log(name, email); // "John Doe", "[email protected]"
console.log(otherInfo); // { id: 1, age: 30, city: "New York" }
Practical Examples
API Request Handler:
function makeApiRequest(url, { method = 'GET', ...options } = {}) {
const defaultOptions = {
headers: {
'Content-Type': 'application/json'
}
};
const requestOptions = {
method,
...defaultOptions,
...options
};
return fetch(url, requestOptions);
}
// Usage
makeApiRequest('/api/users', {
method: 'POST',
body: JSON.stringify({ name: "New User" }),
headers: {
'Authorization': 'Bearer token123'
}
});
React Props Forwarding:
function Button({ children, className, ...otherProps }) {
return (
<button
className={`btn ${className}`}
{...otherProps}
>
{children}
</button>
);
}
// All additional props get passed to the button element
<Button
className="btn-primary"
onClick={handleClick}
disabled={isLoading}
data-testid="submit-btn"
>
Submit
</Button>
Default Parameters: Function Flexibility {#default-parameters}
Default parameters allow you to specify default values for function parameters, making your functions more flexible and reducing the need for manual parameter checking.
Basic Default Parameters
// Before ES6 (manual checking)
function greet(name, greeting) {
name = name || 'Guest';
greeting = greeting || 'Hello';
return greeting + ', ' + name + '!';
}
// ES6 default parameters
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"
console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Bob', 'Hi')); // "Hi, Bob!"
Advanced Default Parameters
Using Expressions:
function createUser(name, id = Date.now(), role = 'user') {
return { name, id, role };
}
function logMessage(message, timestamp = new Date().toISOString()) {
console.log(`[${timestamp}] ${message}`);
}
// Using other parameters
function formatName(first, last, full = `${first} ${last}`) {
return full;
}
Function Calls as Defaults:
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
function createProduct(name, price, sku = generateId()) {
return { name, price, sku };
}
const product1 = createProduct('Laptop', 999);
const product2 = createProduct('Mouse', 25);
console.log(product1); // { name: 'Laptop', price: 999, sku: 'xy7k9m2n1' }
console.log(product2); // { name: 'Mouse', price: 25, sku: 'abc123xyz' }
Default Parameters with Destructuring
// Object destructuring with defaults
function setupDatabase({
host = 'localhost',
port = 5432,
database = 'myapp',
username = 'admin'
} = {}) {
console.log(`Connecting to ${database} at ${host}:${port}`);
// Database connection logic
}
// You can call it with partial configuration
setupDatabase({ database: 'production', host: 'prod-server.com' });
// Or with no configuration at all
setupDatabase();
Array Destructuring with Defaults:
function processCoordinates([lat = 0, lng = 0, elevation = 0] = []) {
return {
latitude: lat,
longitude: lng,
elevation: elevation,
isValid: lat !== 0 || lng !== 0
};
}
console.log(processCoordinates([40.7128, -74.0060]));
// { latitude: 40.7128, longitude: -74.0060, elevation: 0, isValid: true }
console.log(processCoordinates());
// { latitude: 0, longitude: 0, elevation: 0, isValid: false }
Important Notes About Default Parameters
// Defaults are only used for undefined, not for falsy values
function test(value = 'default') {
console.log(value);
}
test(); // 'default'
test(undefined); // 'default'
test(null); // null
test(false); // false
test(0); // 0
test(''); // ''
// Parameters can reference earlier parameters
function createUrl(protocol = 'https', domain, path = '/') {
return `${protocol}://${domain}${path}`;
}
// But not later parameters (ReferenceError)
function invalid(a = b, b = 1) {
return a + b; // ReferenceError: Cannot access 'b' before initialization
}
Enhanced Object Literals {#enhanced-objects}
ES6 introduced several enhancements to object literal syntax that make creating objects more concise and powerful.
Property Shorthand
When the property name matches the variable name, you can use shorthand syntax:
const name = 'John';
const age = 30;
const city = 'Boston';
// Before ES6
const person1 = {
name: name,
age: age,
city: city
};
// ES6 shorthand
const person2 = { name, age, city };
// Mixed usage
const person3 = {
name,
age,
city,
country: 'USA' // regular property
};
Method Shorthand
// Before ES6
const calculator = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
}
};
// ES6 method shorthand
const calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
// Works with arrow functions too (though be careful with 'this')
multiply: (a, b) => a * b
};
Computed Property Names
You can use expressions as property names by wrapping them in square brackets:
const propertyName = 'dynamicKey';
const prefix = 'user';
const suffix = 'Data';
const obj = {
[propertyName]: 'value',
[`${prefix}Name`]: 'Alice',
[`${prefix}${suffix}`]: { /* user data */ },
// Computed methods
[`get${prefix.charAt(0).toUpperCase() + prefix.slice(1)}Info`]() {
return `${this.userName} information`;
}
};
console.log(obj);
// {
// dynamicKey: 'value',
// userName: 'Alice',
// userData: { /* user data */ },
// getUserInfo: [Function]
// }
Practical Examples
API Response Builder:
function buildApiResponse(data, status = 'success', message = null) {
const timestamp = Date.now();
const responseId = Math.random().toString(36).substr(2, 9);
return {
// Property shorthand
data,
status,
message,
timestamp,
// Computed property
[`response_${responseId}`]: true,
// Method shorthand
isSuccess() {
return this.status === 'success';
},
getData() {
return this.isSuccess() ? this.data : null;
},
// Computed method name
[`format${status.charAt(0).toUpperCase() + status.slice(1)}Response`]() {
return {
...this,
formatted: true
};
}
};
}
Form Handler:
class FormHandler {
constructor(formElement) {
this.form = formElement;
this.validators = {};
this.errors = {};
}
addValidator(fieldName, validatorFunction) {
this.validators[fieldName] = validatorFunction;
// Dynamic method creation
this[`validate${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}`] = () => {
const value = this.form[fieldName].value;
const isValid = validatorFunction(value);
return { fieldName, value, isValid };
};
}
getFormData() {
const formData = new FormData(this.form);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
const timestamp = Date.now();
return {
...data,
[`submitted_${timestamp}`]: true,
// Method to validate all fields
validateAll() {
const results = {};
Object.keys(data).forEach(field => {
const methodName = `validate${field.charAt(0).toUpperCase() + field.slice(1)}`;
if (typeof this[methodName] === 'function') {
results[field] = this[methodName]();
}
});
return results;
}
};
}
}
Promises: Taming Asynchronous JavaScript {#promises}
Before ES6, handling asynchronous operations in JavaScript often led to "callback hell" – deeply nested callbacks that were difficult to read and maintain. Promises provide a cleaner, more intuitive way to handle asynchronous operations.
Understanding the Problem
// Callback hell example
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getFinalData(c, function(d) {
// Finally, do something with the data
console.log(d);
}, function(error) {
console.error(error);
});
}, function(error) {
console.error(error);
});
}, function(error) {
console.error(error);
});
}, function(error) {
console.error(error);
});
Promise Basics
A Promise represents a value that may not be available yet but will be resolved at some point in the future.
// Creating a basic Promise
const myPromise = new Promise((resolve, reject) => {
// Simulate async operation
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('Operation successful!');
} else {
reject(new Error('Operation failed!'));
}
}, 1000);
});
// Using the Promise
myPromise
.then(result => {
console.log('Success:', result);
})
.catch(error => {
console.error('Error:', error.message);
});
Promise Chaining
Promises can be chained to handle sequential asynchronous operations:
function fetchUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: 'John Doe', email: '[email protected]' });
}, 500);
});
}
function fetchUserPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: 'First Post', userId },
{ id: 2, title: 'Second Post', userId }
]);
}, 300);
});
}
function fetchPostComments(postId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, text: 'Great post!', postId },
{ id: 2, text: 'Thanks for sharing', postId }
]);
}, 200);
});
}
// Clean promise chaining
fetchUser(123)
.then(user => {
console.log('User:', user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
return fetchPostComments(posts[0].id);
})
.then(comments => {
console.log('Comments:', comments);
})
.catch(error => {
console.error('Error in chain:', error);
})
.finally(() => {
console.log('Operation completed');
});
Promise.all() - Parallel Operations
When you need to wait for multiple independent operations to complete:
const fetchUsers = () => fetch('/api/users').then(r => r.json());
const fetchProducts = () => fetch('/api/products').then(r => r.json());
const fetchOrders = () => fetch('/api/orders').then(r => r.json());
// Wait for all promises to resolve
Promise.all([fetchUsers(), fetchProducts(), fetchOrders()])
.then(([users, products, orders]) => {
console.log('Users:', users);
console.log('Products:', products);
console.log('Orders:', orders);
// All data is now available
initializeApp({ users, products, orders });
})
.catch(error => {
console.error('Failed to load initial data:', error);
showErrorMessage('Failed to load application data');
});
Promise.allSettled() - Handle Partial Failures
Sometimes you want to attempt multiple operations but continue even if some fail:
const promises = [
fetch('/api/users'),
fetch('/api/products'),
fetch('/api/orders-that-might-fail')
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
});