JavaScript has evolved dramatically since ES6 (2015), adding features that make code more readable, maintainable, and powerful. Understanding modern JavaScript—from destructuring to async/await—is essential for writing professional code. This guide covers essential modern JavaScript features from a senior developer's perspective.
Why Modern JavaScript Matters
Modern JavaScript enables:
- Cleaner Code: Less boilerplate, more expressive
- Fewer Bugs: Features that prevent common errors
- Better Performance: Optimized language features
- Async Handling: Elegant asynchronous programming
- Functional Patterns: First-class function support
Variable Declarations
let and const
// const: Cannot be reassigned (use by default)
const API_URL = 'https://api.example.com';
const config = { timeout: 5000 };
// Can modify properties of const objects
config.timeout = 10000; // OK
// config = {}; // Error: Cannot reassign
// let: Can be reassigned (use when needed)
let count = 0;
count = 1; // OK
// Block scoping
if (true) {
const scoped = 'only here';
let alsoScoped = 'only here too';
}
// scoped and alsoScoped are not accessible here
// var hoisting problems (avoid var)
console.log(x); // undefined (hoisted)
var x = 5;
Destructuring
Object Destructuring
const user = {
name: 'John',
email: '[email protected]',
address: {
city: 'NYC',
zip: '10001'
}
};
// Basic destructuring
const { name, email } = user;
console.log(name); // 'John'
// Rename variables
const { name: userName, email: userEmail } = user;
console.log(userName); // 'John'
// Default values
const { role = 'user' } = user;
console.log(role); // 'user' (doesn't exist in user)
// Nested destructuring
const { address: { city, zip } } = user;
console.log(city); // 'NYC'
// Rest operator
const { name: n, ...rest } = user;
console.log(rest); // { email: '...', address: {...} }
// Function parameters
function greet({ name, email }) {
console.log(`Hello ${name} (${email})`);
}
greet(user);
// With defaults in parameters
function createUser({ name = 'Anonymous', role = 'user' } = {}) {
return { name, role };
}
createUser(); // { name: 'Anonymous', role: 'user' }
Array Destructuring
const colors = ['red', 'green', 'blue', 'yellow'];
// Basic array destructuring
const [first, second] = colors;
console.log(first); // 'red'
// Skip elements
const [, , third] = colors;
console.log(third); // 'blue'
// Rest operator
const [primary, ...others] = colors;
console.log(others); // ['green', 'blue', 'yellow']
// Default values
const [a, b, c, d, e = 'purple'] = colors;
console.log(e); // 'purple'
// Swap variables
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1
// Function returns
function getCoordinates() {
return [40.7128, -74.0060];
}
const [lat, lng] = getCoordinates();
Spread Operator
Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// Combine arrays
const combined = [...arr1, ...arr2];
// [1, 2, 3, 4, 5, 6]
// Copy array (shallow)
const copy = [...arr1];
// Add elements
const withExtra = [0, ...arr1, 4];
// [0, 1, 2, 3, 4]
// Convert iterable to array
const chars = [...'hello'];
// ['h', 'e', 'l', 'l', 'o']
// Function arguments
function sum(a, b, c) {
return a + b + c;
}
sum(...arr1); // 6
Objects
const defaults = { theme: 'light', lang: 'en' };
const userPrefs = { theme: 'dark' };
// Merge objects (later properties win)
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'en' }
// Copy object (shallow)
const copy = { ...defaults };
// Add/override properties
const extended = { ...defaults, fontSize: 16 };
// Conditional spreading
const extra = true;
const obj = {
required: 'value',
...(extra && { optional: 'included' })
};
Template Literals
const name = 'John';
const age = 30;
// String interpolation
const greeting = `Hello, ${name}! You are ${age} years old.`;
// Expressions in templates
const message = `Next year you'll be ${age + 1}`;
// Multi-line strings
const html = `
<div class="card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`;
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] ? `<strong>${values[i]}</strong>` : '';
return result + str + value;
}, '');
}
const highlighted = highlight`Hello ${name}, you are ${age}!`;
// 'Hello <strong>John</strong>, you are <strong>30</strong>!'
Arrow Functions
// Basic syntax
const add = (a, b) => a + b;
// Single parameter (no parentheses needed)
const double = x => x * 2;
// No parameters
const getRandom = () => Math.random();
// Multi-line (need braces and return)
const calculate = (a, b) => {
const sum = a + b;
const product = a * b;
return { sum, product };
};
// Return object literal (wrap in parentheses)
const createUser = (name, email) => ({ name, email });
// Array methods
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
// Lexical this (no own this binding)
const obj = {
name: 'Object',
greet() {
// Arrow function inherits this from greet()
setTimeout(() => {
console.log(this.name); // 'Object'
}, 100);
}
};
Array Methods
const users = [
{ id: 1, name: 'John', age: 30 },
{ id: 2, name: 'Jane', age: 25 },
{ id: 3, name: 'Bob', age: 35 }
];
// map: Transform each element
const names = users.map(u => u.name);
// ['John', 'Jane', 'Bob']
// filter: Keep matching elements
const adults = users.filter(u => u.age >= 30);
// [{ id: 1, ... }, { id: 3, ... }]
// find: Get first matching element
const john = users.find(u => u.name === 'John');
// { id: 1, name: 'John', age: 30 }
// findIndex: Get index of first match
const johnIndex = users.findIndex(u => u.name === 'John');
// 0
// some: Check if any match
const hasAdult = users.some(u => u.age >= 18);
// true
// every: Check if all match
const allAdults = users.every(u => u.age >= 18);
// true
// reduce: Accumulate to single value
const totalAge = users.reduce((sum, u) => sum + u.age, 0);
// 90
// Chaining
const result = users
.filter(u => u.age >= 30)
.map(u => u.name)
.sort();
// ['Bob', 'John']
// includes
const hasJohn = names.includes('John'); // true
// flat: Flatten nested arrays
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.flat(); // [1, 2, 3, 4, 5]
// flatMap: map + flat
const sentences = ['Hello World', 'Goodbye World'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'World', 'Goodbye', 'World']
Object Shorthand
const name = 'John';
const age = 30;
// Property shorthand
const user = { name, age };
// Same as: { name: name, age: age }
// Method shorthand
const obj = {
greet() {
return 'Hello';
},
// Same as: greet: function() { return 'Hello'; }
};
// Computed property names
const key = 'dynamicKey';
const dynamic = {
[key]: 'value',
[`${key}_backup`]: 'backup'
};
// { dynamicKey: 'value', dynamicKey_backup: 'backup' }
Optional Chaining and Nullish Coalescing
const user = {
name: 'John',
address: {
city: 'NYC'
}
};
// Optional chaining (?.)
const zip = user?.address?.zip; // undefined (no error)
const country = user?.address?.country?.code; // undefined
// Without optional chaining (old way)
const zipOld = user && user.address && user.address.zip;
// With arrays
const first = users?.[0]?.name;
// With functions
const result = obj?.method?.();
// Nullish coalescing (??)
// Returns right side only if left is null or undefined
const value = null ?? 'default'; // 'default'
const zero = 0 ?? 'default'; // 0 (0 is not nullish)
const empty = '' ?? 'default'; // '' (empty string is not nullish)
// Compare with || (falsy check)
const valueOr = null || 'default'; // 'default'
const zeroOr = 0 || 'default'; // 'default' (0 is falsy!)
// Combining
const setting = config?.theme ?? 'light';
Async/Await
// Basic async function
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Error handling
async function fetchUserSafe(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
// Parallel execution
async function fetchAll() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
return { users, posts };
}
// Sequential execution
async function fetchSequential() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id); // Depends on user
return { user, posts };
}
// Async arrow function
const getUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
Modules (Import/Export)
// Named exports (utils.js)
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator { }
// Default export (User.js)
export default class User {
constructor(name) {
this.name = name;
}
}
// Named imports
import { PI, add } from './utils.js';
// Rename imports
import { add as sum } from './utils.js';
// Import all as namespace
import * as utils from './utils.js';
utils.add(1, 2);
// Default import
import User from './User.js';
// Mixed
import User, { helper } from './User.js';
// Dynamic import
const module = await import('./heavy-module.js');
Key Takeaways
- Use const by default: Only use let when reassignment is needed
- Destructure early: Clean up function parameters and assignments
- Spread for immutability: Create new arrays/objects instead of mutating
- Arrow functions for callbacks: Cleaner syntax, lexical this
- Optional chaining for safety: Avoid "cannot read property of undefined"
- async/await over promises: More readable async code
Modern JavaScript features aren't just syntactic sugar—they enable patterns that make code more robust and maintainable.