Module 26: ES6+ Modern Features
ES6 (ECMAScript 2015) and beyond introduced revolutionary features that modernized JavaScript. These features make code more readable, maintainable, and powerful.
1. Destructuring
1.1 Array Destructuring
Extract values from arrays into individual variables.
// Basic destructuring
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
console.log(first); // 'red'
console.log(second); // 'green'
// Skipping elements
const [primary, , tertiary] = colors;
console.log(primary); // 'red'
console.log(tertiary); // 'blue'
// Rest operator
const numbers = [1, 2, 3, 4, 5];
const [one, two, ...rest] = numbers;
console.log(one); // 1
console.log(two); // 2
console.log(rest); // [3, 4, 5]
// Default values
const [a = 1, b = 2, c = 3] = [10];
console.log(a); // 10
console.log(b); // 2 (default)
console.log(c); // 3 (default)
// Swapping variables
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1
1.2 Object Destructuring
Extract properties from objects.
// Basic destructuring
const user = {
name: 'John',
age: 30,
email: 'john@example.com'
};
const { name, age, email } = user;
console.log(name); // 'John'
console.log(age); // 30
// Renaming variables
const { name: userName, age: userAge } = user;
console.log(userName); // 'John'
console.log(userAge); // 30
// Default values
const { name, country = 'USA' } = user;
console.log(country); // 'USA'
// Nested destructuring
const employee = {
name: 'Jane',
position: 'Developer',
address: {
city: 'New York',
country: 'USA'
}
};
const {
name,
address: { city, country }
} = employee;
console.log(city); // 'New York'
console.log(country); // 'USA'
// Rest operator
const { name: personName, ...details } = user;
console.log(personName); // 'John'
console.log(details); // { age: 30, email: '...' }
1.3 Function Parameter Destructuring
// Object parameters
function createUser({ name, age, email }) {
console.log(`Creating user: ${name}, ${age}`);
}
createUser({ name: 'John', age: 30, email: 'john@example.com' });
// With defaults
function configure({
host = 'localhost',
port = 3000,
secure = false
} = {}) {
console.log(`${secure ? 'https' : 'http'}://${host}:${port}`);
}
configure(); // http://localhost:3000
configure({ host: 'example.com', secure: true }); // https://example.com:3000
// Array parameters
function displayCoordinates([x, y, z = 0]) {
console.log(`x: ${x}, y: ${y}, z: ${z}`);
}
displayCoordinates([10, 20]); // x: 10, y: 20, z: 0
Use destructuring to make function parameters more explicit and self-documenting, especially when dealing with configuration objects.
2. Spread and Rest Operators
2.1 Spread Operator (...)
Expand iterables into individual elements.
// Array spreading
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copying arrays
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] (unchanged)
console.log(copy); // [1, 2, 3, 4]
// Array manipulation
const numbers = [3, 1, 4, 1, 5];
console.log(Math.max(...numbers)); // 5
console.log(Math.min(...numbers)); // 1
// Converting to array
const str = 'hello';
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']
const nodeList = document.querySelectorAll('div');
const divArray = [...nodeList]; // Convert NodeList to Array
2.2 Object Spread
// Object spreading
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: 'dark', lang: 'fr', fontSize: 14 }
// Shallow copying
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.b.c = 3;
console.log(original.b.c); // 3 (nested objects are shared!)
// Adding/overriding properties
const user = { name: 'John', age: 30 };
const updatedUser = { ...user, age: 31, city: 'NYC' };
console.log(updatedUser);
// { name: 'John', age: 31, city: 'NYC' }
// Merging multiple objects
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const merged = { ...obj1, ...obj2, ...obj3 };
console.log(merged); // { a: 1, b: 2, c: 3 }
2.3 Rest Parameters
Collect multiple elements into an array.
// Function rest parameters
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
// Mixed parameters
function greet(greeting, ...names) {
return `${greeting} ${names.join(', ')}!`;
}
console.log(greet('Hello', 'John', 'Jane', 'Bob'));
// "Hello John, Jane, Bob!"
// Object rest
const { x, y, ...rest } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(rest); // { a: 3, b: 4 }
The same ... syntax serves different purposes: spread expands elements, while rest collects them. Context determines the behavior!
3. Template Literals
3.1 String Interpolation
// Basic interpolation
const name = 'John';
const age = 30;
const message = `Hello, I'm ${name} and I'm ${age} years old.`;
console.log(message);
// Expressions in templates
const a = 10, b = 20;
console.log(`Sum: ${a + b}`); // "Sum: 30"
console.log(`Max: ${Math.max(a, b)}`); // "Max: 20"
// Function calls
function uppercase(str) {
return str.toUpperCase();
}
console.log(`Name: ${uppercase(name)}`); // "Name: JOHN"
3.2 Multi-line Strings
// Multi-line without escaping
const html = `
<div class="container">
<h1>Welcome</h1>
<p>This is a paragraph</p>
</div>
`;
const sql = `
SELECT users.name, orders.total
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE orders.total > 100
`;
3.3 Tagged Templates
Advanced template processing.
// Basic tagged template
function tag(strings, ...values) {
console.log(strings); // Array of string parts
console.log(values); // Array of interpolated values
return strings[0] + values[0] + strings[1];
}
const result = tag`Hello ${name}!`;
// Practical example: SQL escaping
function sql(strings, ...values) {
return strings.reduce((query, str, i) => {
const value = values[i - 1];
const escaped = typeof value === 'string'
? value.replace(/'/g, "''")
: value;
return query + escaped + str;
});
}
const userId = 1;
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
// HTML escaping
function html(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i - 1];
const escaped = String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
return result + escaped + str;
});
}
const userInput = '<script>alert("XSS")</script>';
const safe = html`<div>${userInput}</div>`;
// <div><script>alert("XSS")</script></div>
// Styling library (styled-components pattern)
function css(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] || '');
}, '');
}
const primaryColor = '#007bff';
const styles = css`
.button {
background-color: ${primaryColor};
padding: 10px 20px;
}
`;
Tagged templates are used in popular libraries like styled-components, graphql-tag, and sql-template-strings for domain-specific languages.
4. Arrow Functions (Advanced)
4.1 Implicit Returns
// Explicit return
const double = (x) => {
return x * 2;
};
// Implicit return
const double = x => x * 2;
// Object literal (wrap in parentheses)
const makeUser = (name, age) => ({ name, age });
console.log(makeUser('John', 30)); // { name: 'John', age: 30 }
// Multi-line implicit return
const sum = (a, b) => (
a + b
);
4.2 Lexical this
// Problem with regular functions
const counter = {
count: 0,
increment: function() {
setTimeout(function() {
this.count++; // 'this' is undefined or window!
console.log(this.count);
}, 1000);
}
};
// Solution 1: Arrow function
const counter = {
count: 0,
increment: function() {
setTimeout(() => {
this.count++; // 'this' refers to counter object
console.log(this.count);
}, 1000);
}
};
// Solution 2: bind
const counter = {
count: 0,
increment: function() {
setTimeout(function() {
this.count++;
}.bind(this), 1000);
}
};
// Real-world example
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.start(); // Works correctly with arrow function
Arrow functions don't have their own this, arguments, super, or new.target. Don't use them as methods when you need dynamic this.
5. Default Parameters
5.1 Basic Default Values
// Before ES6
function greet(name) {
name = name || 'Guest';
return `Hello, ${name}!`;
}
// ES6 default parameters
function greet(name = 'Guest') {
return `Hello, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"
console.log(greet('John')); // "Hello, John!"
// Multiple defaults
function createUser(name = 'Anonymous', age = 0, role = 'user') {
return { name, age, role };
}
console.log(createUser());
// { name: 'Anonymous', age: 0, role: 'user' }
5.2 Expression Defaults
// Function call as default
function getDefaultName() {
return 'User' + Math.floor(Math.random() * 1000);
}
function createUser(name = getDefaultName()) {
return { name };
}
// Earlier parameters as defaults
function calculatePrice(price, tax = price * 0.1, tip = price * 0.15) {
return price + tax + tip;
}
console.log(calculatePrice(100));
// 125 (100 + 10 + 15)
// Destructured parameters with defaults
function configure({
host = 'localhost',
port = 3000,
secure = false
} = {}) {
return `${secure ? 'https' : 'http'}://${host}:${port}`;
}
console.log(configure());
// "http://localhost:3000"
6. Enhanced Object Literals
6.1 Shorthand Properties
const name = 'John';
const age = 30;
// Before ES6
const user = {
name: name,
age: age
};
// ES6 shorthand
const user = { name, age };
// Dynamic property names
const key = 'email';
const user = {
name,
age,
[key]: 'john@example.com'
};
console.log(user.email); // 'john@example.com'
6.2 Method Shorthand
// Before ES6
const calculator = {
add: function(a, b) {
return a + b;
},
multiply: function(a, b) {
return a * b;
}
};
// ES6 shorthand
const calculator = {
add(a, b) {
return a + b;
},
multiply(a, b) {
return a * b;
}
};
// Getters and setters
const person = {
firstName: 'John',
lastName: 'Doe',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ');
}
};
console.log(person.fullName); // "John Doe"
person.fullName = 'Jane Smith';
console.log(person.firstName); // "Jane"
6.3 Computed Property Names
const prop = 'name';
const value = 'John';
const obj = {
[prop]: value,
[`get${prop.charAt(0).toUpperCase() + prop.slice(1)}`]() {
return this[prop];
}
};
console.log(obj.name); // 'John'
console.log(obj.getName()); // 'John'
// Dynamic keys from array
const keys = ['name', 'age', 'email'];
const values = ['John', 30, 'john@example.com'];
const user = keys.reduce((obj, key, i) => ({
...obj,
[key]: values[i]
}), {});
console.log(user);
// { name: 'John', age: 30, email: 'john@example.com' }
7. Symbols
7.1 Creating Symbols
// Unique identifiers
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false
// With description
const sym = Symbol('description');
console.log(sym.toString()); // "Symbol(description)"
// Global symbol registry
const globalSym = Symbol.for('app.id');
const sameSym = Symbol.for('app.id');
console.log(globalSym === sameSym); // true
console.log(Symbol.keyFor(globalSym)); // "app.id"
7.2 Symbol Use Cases
// Private-like properties
const _private = Symbol('private');
class User {
constructor(name) {
this.name = name;
this[_private] = 'secret data';
}
getPrivate() {
return this[_private];
}
}
const user = new User('John');
console.log(user.name); // 'John'
console.log(user[_private]); // undefined (not really private, but hidden)
console.log(user.getPrivate()); // 'secret data'
// Object.keys() doesn't see symbols
console.log(Object.keys(user)); // ['name']
// But Object.getOwnPropertySymbols() does
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(private)]
// Well-known symbols
const obj = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
for (const value of obj) {
console.log(value); // 1, 2, 3
}
// Custom toString behavior
class MyArray {
[Symbol.toStringTag] = 'MyArray';
}
console.log(Object.prototype.toString.call(new MyArray()));
// "[object MyArray]"
8. Iterators and Generators
8.1 Iterators
// Creating an iterator
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Manual iteration
const iterator = range[Symbol.iterator]();
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
8.2 Generators
Simplified iterator creation.
// Generator function
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.next()); // { done: false, value: 2 }
console.log(gen.next()); // { done: false, value: 3 }
console.log(gen.next()); // { done: true, value: undefined }
// Using in loops
for (const num of numberGenerator()) {
console.log(num); // 1, 2, 3
}
// Infinite generators
function* infiniteCounter() {
let count = 0;
while (true) {
yield count++;
}
}
const counter = infiniteCounter();
console.log(counter.next().value); // 0
console.log(counter.next().value); // 1
// Generator with parameters
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
// Delegating generators
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield* gen1(); // Delegate to gen1
yield 3;
yield 4;
}
console.log([...gen2()]); // [1, 2, 3, 4]
// Async iteration
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
(async () => {
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3
}
})();
Generators are perfect for working with large datasets or infinite sequences because they produce values on-demand.
9. Optional Chaining and Nullish Coalescing
9.1 Optional Chaining (?.)
// Without optional chaining
const user = {
name: 'John',
address: {
street: 'Main St'
}
};
// Risky: might throw error
// const city = user.address.city.name;
// Safe but verbose
const city = user && user.address && user.address.city;
// Optional chaining (ES2020)
const city = user?.address?.city;
console.log(city); // undefined (no error!)
// With arrays
const users = [{name: 'John'}, {name: 'Jane'}];
console.log(users[0]?.name); // 'John'
console.log(users[5]?.name); // undefined
// With function calls
const obj = {
method() {
return 'exists';
}
};
console.log(obj.method?.()); // 'exists'
console.log(obj.missing?.()); // undefined
console.log(obj.method()); // 'exists'
// obj.missing(); // Error!
// Real-world example
function getUserCity(user) {
return user?.profile?.address?.city ?? 'Unknown';
}
9.2 Nullish Coalescing (??)
// Problem with || operator
const value1 = 0 || 'default'; // 'default' (0 is falsy!)
const value2 = '' || 'default'; // 'default' ('' is falsy!)
const value3 = false || 'default'; // 'default' (false is falsy!)
// Nullish coalescing only checks for null/undefined
const value1 = 0 ?? 'default'; // 0
const value2 = '' ?? 'default'; // ''
const value3 = false ?? 'default'; // false
const value4 = null ?? 'default'; // 'default'
const value5 = undefined ?? 'default'; // 'default'
// Configuration with defaults
function createConfig(options) {
return {
host: options?.host ?? 'localhost',
port: options?.port ?? 3000,
debug: options?.debug ?? false
};
}
console.log(createConfig({}));
// { host: 'localhost', port: 3000, debug: false }
console.log(createConfig({ port: 8080, debug: true }));
// { host: 'localhost', port: 8080, debug: true }
// Can't combine with && or ||
// const value = true && undefined ?? 'default'; // SyntaxError!
const value = (true && undefined) ?? 'default'; // OK with parentheses
Optional chaining (?.) and nullish coalescing (??) were introduced in ES2020. Ensure your target environment supports them or use a transpiler.
10. Real-World Patterns
10.1 Configuration Objects
// Using all modern features together
function createApp({
name = 'MyApp',
version = '1.0.0',
features = [],
config: {
debug = false,
apiUrl = 'https://api.example.com'
} = {}
} = {}) {
return {
name,
version,
features: [...features],
config: {
debug,
apiUrl,
timestamp: Date.now()
},
start() {
console.log(`Starting ${this.name} v${this.version}`);
console.log(`Debug mode: ${this.config.debug}`);
console.log(`API URL: ${this.config.apiUrl}`);
console.log(`Features: ${this.features.join(', ') || 'none'}`);
},
addFeature(feature) {
this.features = [...this.features, feature];
}
};
}
// Usage
const app = createApp({
name: 'TodoApp',
config: { debug: true },
features: ['auth', 'notifications']
});
app.start();
app.addFeature('sync');
10.2 Data Processing Pipeline
// Using generators for lazy evaluation
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* map(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}
function* take(iterable, count) {
let i = 0;
for (const item of iterable) {
if (i++ >= count) break;
yield item;
}
}
// Pipeline
const numbers = function* () {
let n = 1;
while (true) yield n++;
};
const result = [
...take(
map(
filter(numbers(), n => n % 2 === 0),
n => n * n
),
5
)
];
console.log(result); // [4, 16, 36, 64, 100] (first 5 even squares)
// More readable with helper
function pipeline(source, ...operations) {
return operations.reduce((acc, op) => op(acc), source);
}
const result2 = pipeline(
numbers(),
iter => filter(iter, n => n % 2 === 0),
iter => map(iter, n => n * n),
iter => take(iter, 5)
);
console.log([...result2]);
10.3 API Client with Modern Syntax
class APIClient {
constructor({
baseURL,
headers = {},
timeout = 5000
}) {
this.baseURL = baseURL;
this.headers = {
'Content-Type': 'application/json',
...headers
};
this.timeout = timeout;
}
async request(endpoint, {
method = 'GET',
body,
headers = {}
} = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
method,
headers: { ...this.headers, ...headers },
...(body && { body: JSON.stringify(body) })
};
try {
const response = await fetch(url, config);
const data = await response.json();
return {
ok: response.ok,
status: response.status,
data
};
} catch (error) {
return {
ok: false,
error: error?.message ?? 'Unknown error'
};
}
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, body, options) {
return this.request(endpoint, { ...options, method: 'POST', body });
}
put(endpoint, body, options) {
return this.request(endpoint, { ...options, method: 'PUT', body });
}
delete(endpoint, options) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Usage
const api = new APIClient({
baseURL: 'https://api.example.com',
headers: { Authorization: 'Bearer token123' }
});
const { data, error } = await api.get('/users/123');
if (error) {
console.error('Error:', error);
} else {
console.log('User:', data?.name ?? 'Unknown');
}
Summary
In this module, you learned:
- ✅ Array and object destructuring for cleaner code
- ✅ Spread and rest operators for flexible data manipulation
- ✅ Template literals and tagged templates
- ✅ Arrow functions and lexical this binding
- ✅ Default parameters and enhanced object literals
- ✅ Symbols for unique identifiers
- ✅ Iterators and generators for custom iteration
- ✅ Optional chaining and nullish coalescing (ES2020+)
- ✅ Real-world patterns combining modern features
In Module 27, you'll learn about Memory Management and Performance, understanding how to write efficient JavaScript code and optimize applications.
Practice Exercises
- Refactor a legacy codebase to use destructuring and spread operators
- Create a utility library using arrow functions and default parameters
- Build a template engine using tagged templates
- Implement a custom iterator for a data structure
- Create generators for infinite sequences and lazy evaluation
- Build an API client using all ES6+ features
- Create a data pipeline using generators
- Implement optional chaining in a nested data access utility