Chapter 14: Modern JavaScript Features (ES6+)
· 14 min read
Chapter 14: Modern JavaScript Features (ES6+)
Destructuring with Default Values and Renaming
// Complex destructuring scenarios
const config = {
api: {
baseUrl: "https://api.example.com",
timeout: 5000
},
features: {
darkMode: true
}
};
// Nested destructuring with defaults and renaming
const {
api: {
baseUrl: apiUrl = "https://default.com",
timeout = 3000,
retries = 3 // Not in original object
},
features: {
darkMode = false,
notifications: enableNotifications = true
} = {} // Default to empty object if features is undefined
} = config;
console.log(apiUrl, timeout, retries, darkMode, enableNotifications);
Advanced Array Methods
const users = [
{ id: 1, name: "Alice", age: 25, active: true },
{ id: 2, name: "Bob", age: 30, active: false },
{ id: 3, name: "Charlie", age: 35, active: true },
{ id: 4, name: "Diana", age: 28, active: true }
];
// Chaining array methods
const result = users
.filter(user => user.active) // Only active users
.map(user => ({ ...user, ageGroup: user.age < 30 ? 'young' : 'mature' }))
.sort((a, b) => a.age - b.age) // Sort by age
.reduce((acc, user) => { // Group by age group
const group = user.ageGroup;
acc[group] = acc[group] || [];
acc[group].push(user);
return acc;
}, {});
console.log(result);
// flatMap - map and flatten in one step
const nested = [[1, 2], [3, 4], [5, 6]];
const flattened = nested.flatMap(arr => arr.map(x => x * 2));
console.log(flattened); // [2, 4, 6, 8, 10, 12]
// Array.from with mapping function
const range = Array.from({ length: 5 }, (_, i) => i * 2);
console.log(range); // [0, 2, 4, 6, 8]
Proxy and Reflect
// Proxy - intercept and customize operations
const user = {
name: "Alice",
age: 30
};
const userProxy = new Proxy(user, {
get(target, property) {
console.log(`Getting ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`Setting ${property} to ${value}`);
if (property === 'age' && value < 0) {
throw new Error("Age cannot be negative");
}
target[property] = value;
return true;
},
has(target, property) {
console.log(`Checking if ${property} exists`);
return property in target;
}
});
console.log(userProxy.name); // "Getting name" → "Alice"
userProxy.age = 31; // "Setting age to 31"
console.log('name' in userProxy); // "Checking if name exists" → true
// Reflect - programmatic object operations
const obj = { a: 1, b: 2 };
console.log(Reflect.has(obj, 'a')); // true
console.log(Reflect.ownKeys(obj)); // ["a", "b"]
Reflect.set(obj, 'c', 3);
console.log(obj); // { a: 1, b: 2, c: 3 }
WeakMap and WeakSet
// WeakMap - weak references to objects as keys
const privateData = new WeakMap();
class BankAccount {
constructor(balance) {
// Store private data using WeakMap
privateData.set(this, { balance, transactions: [] });
}
deposit(amount) {
const data = privateData.get(this);
data.balance += amount;
data.transactions.push(`Deposited ${amount}`);
return data.balance;
}
getBalance() {
return privateData.get(this).balance;
}
getTransactions() {
return privateData.get(this).transactions.slice(); // Return copy
}
}
const account = new BankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
// Private data is truly private - no way to access it directly
console.log(account.balance); // undefined
// WeakSet - weak references to objects
const visitedNodes = new WeakSet();
function traverse(node) {
if (visitedNodes.has(node)) {
return; // Avoid cycles
}
visitedNodes.add(node);
// Process node...
if (node.children) {
node.children.forEach(child => traverse(child));
}
}
Optional Chaining and Nullish Coalescing
// Optional chaining (?.) - safely access nested properties
const user = {
profile: {
social: {
twitter: "@alice"
}
}
};
// Traditional way (verbose and error-prone)
const twitter1 = user && user.profile && user.profile.social && user.profile.social.twitter;
// Optional chaining (clean and safe)
const twitter2 = user?.profile?.social?.twitter;
const instagram = user?.profile?.social?.instagram; // undefined (no error)
// Works with arrays and function calls too
const firstHobby = user?.hobbies?.[0];
const result = user?.getName?.();
// Nullish coalescing (??) - default values for null/undefined
const username = user?.name ?? "Anonymous";
const theme = user?.preferences?.theme ?? "light";
// Different from || operator
console.log("" || "default"); // "default" (empty string is falsy)
console.log("" ?? "default"); // "" (empty string is not nullish)
console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"
Dynamic Imports and Top-level Await
// Dynamic imports - load modules conditionally
async function loadModule(condition) {
if (condition) {
const module = await import('./heavy-module.js');
return module.default();
}
}
// Top-level await (in modules)
// utils.js
const data = await fetch('https://api.example.com/data').then(r => r.json());
export { data };
// Code splitting with dynamic imports
const LazyComponent = lazy(() => import('./LazyComponent'));
Logical Assignment Operators
// Logical assignment operators (ES2021)
let a = null;
let b = 0;
let c = "";
// Nullish coalescing assignment
a ??= "default"; // Only assigns if a is null or undefined
console.log(a); // "default"
// Logical AND assignment
b &&= 42; // Only assigns if b is truthy
console.log(b); // 0 (not assigned because 0 is falsy)
// Logical OR assignment
c ||= "fallback"; // Only assigns if c is falsy
console.log(c); // "fallback"
// Practical example
class Config {
constructor(options = {}) {
this.debug = options.debug ?? false;
this.timeout = options.timeout ?? 5000;
this.retries = options.retries ?? 3;
// Set default headers
this.headers ??= {};
this.headers['Content-Type'] ??= 'application/json';
// Enable caching if not explicitly disabled
this.cache &&= true;
}
}
String Methods and Pattern Matching
// String padding
const num = "42";
console.log(num.padStart(5, "0")); // "00042"
console.log(num.padEnd(5, "!")); // "42!!!"
// String matching
const text = "Hello World JavaScript";
// includes, startsWith, endsWith
console.log(text.includes("World")); // true
console.log(text.startsWith("Hello")); // true
console.log(text.endsWith("Script")); // true
// replaceAll (ES2021)
const sentence = "The quick brown fox jumps over the lazy dog";
console.log(sentence.replaceAll("the", "THE")); // Replaces all occurrences
// matchAll for global regex matches
const regex = /\b\w{4}\b/g; // 4-letter words
const matches = [...sentence.matchAll(regex)];
console.log(matches.map(match => match[0])); // ["over", "lazy"]
BigInt for Large Numbers
// BigInt for numbers larger than Number.MAX_SAFE_INTEGER
const bigNumber = 9007199254740991n; // Note the 'n' suffix
const anotherBig = BigInt("9007199254740992");
console.log(bigNumber + 1n); // 9007199254740992n
console.log(bigNumber * 2n); // 18014398509481982n
// Can't mix BigInt with regular numbers
// console.log(bigNumber + 1); // TypeError!
// Convert between BigInt and Number
console.log(Number(bigNumber)); // May lose precision
console.log(BigInt(123)); // 123n
// Use cases: cryptography, precise calculations
function factorial(n) {
if (n <= 1n) return 1n;
return n * factorial(n - 1n);
}
console.log(factorial(20n)); // 2432902008176640000n
Private Class Fields
// Private fields and methods (ES2022)
class ModernUser {
// Private fields
#id;
#password;
// Private static field
static #instanceCount = 0;
constructor(username, password) {
this.username = username;
this.#password = this.#hashPassword(password);
this.#id = ++ModernUser.#instanceCount;
}
// Private method
#hashPassword(password) {
return `hashed_${password}`;
}
// Public method using private fields
authenticate(password) {
return this.#hashPassword(password) === this.#password;
}
getId() {
return this.#id;
}
// Static method using private static field
static getTotalUsers() {
return ModernUser.#instanceCount;
}
}
const user = new ModernUser("alice", "secret123");
console.log(user.authenticate("secret123")); // true
console.log(user.getId()); // 1
// console.log(user.#id); // SyntaxError: Private field '#id' must be declared in an enclosing class
Numeric Separators and Other Literals
// Numeric separators for readability
const million = 1_000_000;
const binary = 0b1010_0001;
const octal = 0o755_000;
const hex = 0xFF_EC_DE_5E;
console.log(million); // 1000000
// Template literal improvements
const createQuery = (table, fields) => `
SELECT ${fields.join(', ')}
FROM ${table}
WHERE active = 1
ORDER BY created_at DESC
`;
const query = createQuery('users', ['id', 'name', 'email']);
console.log(query);
Array.at() Method
// Array.at() for easier access to elements from the end
const arr = [1, 2, 3, 4, 5];
// Traditional way
console.log(arr[arr.length - 1]); // 5 (last element)
console.log(arr[arr.length - 2]); // 4 (second to last)
// With Array.at()
console.log(arr.at(-1)); // 5 (last element)
console.log(arr.at(-2)); // 4 (second to last)
console.log(arr.at(0)); // 1 (first element)
// Works with strings too
const str = "hello";
console.log(str.at(-1)); // "o"
Object.hasOwn()
// Object.hasOwn() - safer alternative to hasOwnProperty
const obj = {
name: "Alice",
age: 30
};
// Traditional way (can be problematic)
console.log(obj.hasOwnProperty("name")); // true
// What if hasOwnProperty is overridden?
obj.hasOwnProperty = () => false;
console.log(obj.hasOwnProperty("name")); // false (wrong!)
// Object.hasOwn() is safer
console.log(Object.hasOwn(obj, "name")); // true (correct)
// Works even with null prototype objects
const nullObj = Object.create(null);
nullObj.prop = "value";
// nullObj.hasOwnProperty("prop"); // TypeError!
console.log(Object.hasOwn(nullObj, "prop")); // true
Performance Considerations with Modern Features
// Optional chaining has performance cost
// ❌ Overuse can be expensive
function processUser(user) {
return {
name: user?.profile?.personal?.name,
email: user?.profile?.contact?.email,
phone: user?.profile?.contact?.phone,
address: user?.profile?.contact?.address?.street
};
}
// ✅ Better approach
function processUserOptimized(user) {
const profile = user?.profile;
if (!profile) return null;
const personal = profile.personal;
const contact = profile.contact;
return {
name: personal?.name,
email: contact?.email,
phone: contact?.phone,
address: contact?.address?.street
};
}
// Proxy performance consideration
// ❌ Proxy adds overhead
const expensiveProxy = new Proxy({}, {
get(target, prop) {
console.log(`Accessing ${prop}`);
return target[prop];
}
});
// ✅ Use Proxy judiciously for specific use cases
Next Chapter: Chapter 15: Performance and Best Practices
Previous Chapter: Chapter 13: The Interview Gauntlet - Common Tricks and Gotchas
Table of Contents: JavaScript Guide