Advanced JavaScript Features
Note: This post is based on my old programming study notes when I taught myself.
Destructuring Assignment
Destructuring allows extracting values from arrays or properties from objects into distinct variables.
Array Destructuring
// Basic array 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'
// Default values
const [a, b, c, d = "yellow"] = colors;
console.log(d); // 'yellow'
// Rest pattern
const numbers = [1, 2, 3, 4, 5];
const [head, ...tail] = numbers;
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// Swapping variables
let x = 1,
y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1
Object Destructuring
// Basic object destructuring
const person = { name: "John", age: 30, city: "New York" };
const { name, age } = person;
console.log(name); // 'John'
console.log(age); // 30
// Renaming variables
const { name: fullName, age: years } = person;
console.log(fullName); // 'John'
console.log(years); // 30
// Default values
const { name, age, country = "USA" } = person;
console.log(country); // 'USA'
// Nested destructuring
const user = {
id: 1,
profile: {
name: "Alice",
settings: {
theme: "dark",
},
},
};
const {
profile: {
name,
settings: { theme },
},
} = user;
console.log(name); // 'Alice'
console.log(theme); // 'dark'
// Function parameter destructuring
function greet({ name, age }) {
return `Hello ${name}, you are ${age} years old`;
}
greet({ name: "Bob", age: 25 }); // 'Hello Bob, you are 25 years old'
Spread and Rest Operators
Both use ... syntax but serve opposite purposes: spread expands, rest collects.
Spread Operator
// Array spreading
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Object spreading (creates new object without mutation)
const slime = { name: "슬라임" };
const cuteSlime = {
...slime,
attribute: "cute",
};
console.log(cuteSlime); // { name: '슬라임', attribute: 'cute' }
// Function arguments
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6
// Copying arrays (shallow copy)
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] (unchanged)
// Converting NodeList to Array
const divs = document.querySelectorAll("div");
const divArray = [...divs];
// Finding max/min in arrays
const scores = [89, 76, 92, 58];
console.log(Math.max(...scores)); // 92
Rest Operator
// Function parameters
function multiply(multiplier, ...numbers) {
return numbers.map((num) => num * multiplier);
}
console.log(multiply(2, 3, 4, 5)); // [6, 8, 10]
// Array destructuring
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
// Object destructuring
const { name, ...otherProps } = { name: "John", age: 30, city: "NYC" };
console.log(name); // 'John'
console.log(otherProps); // { age: 30, city: 'NYC' }
// Dynamic object creation
function createUser(name, ...attributes) {
return {
name,
attributes: attributes.reduce((acc, attr) => ({ ...acc, ...attr }), {}),
};
}
Enhanced Object Literals
Modern JavaScript provides shorthand syntax for creating objects.
const name = "John";
const age = 30;
// Property shorthand
const person = { name, age }; // Instead of { name: name, age: age }
// Method shorthand
const calculator = {
// Old way: add: function(a, b) { return a + b; }
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
};
// Computed property names
const prefix = "user";
const id = 123;
const user = {
[prefix + "Id"]: id, // userId: 123
[`${prefix}Name`]: "Alice", // userName: 'Alice'
[Symbol.iterator]: function* () {
yield this.userId;
yield this.userName;
},
};
// Dynamic method names
const methodName = "getData";
const api = {
baseUrl: "/api/data",
[methodName]() {
return fetch(this.baseUrl);
},
};
// Object destructuring in enhanced literals
const config = {
// Shorthand with default values
port: process.env.PORT || 3000,
host: process.env.HOST || "localhost",
// Method with destructuring parameters
start({ port, host } = this) {
console.log(`Server running on ${host}:${port}`);
},
};
Default Parameters
Function parameters with fallback values when arguments are undefined.
// Basic default parameters
function greet(name = "Guest", message = "Hello") {
return `${message}, ${name}!`;
}
console.log(greet()); // 'Hello, Guest!'
console.log(greet("Alice")); // 'Hello, Alice!'
console.log(greet("Bob", "Hi")); // 'Hi, Bob!'
// Default parameters can reference other parameters
function createUrl(
protocol = "https",
host = "localhost",
port = getDefaultPort(protocol)
) {
return `${protocol}://${host}:${port}`;
}
function getDefaultPort(protocol) {
return protocol === "https" ? 443 : 80;
}
// Default parameters with destructuring
function processUser({ name, age = 18, role = "user", permissions = ["read"] } = {}) {
return { name, age, role, permissions };
}
console.log(processUser()); // { name: undefined, age: 18, role: 'user', permissions: ['read'] }
console.log(processUser({ name: "Alice", age: 25 }));
// Complex default values
function createConfig({
env = "development",
debug = env === "development",
apiUrl = env === "production" ? "https://api.prod.com" : "https://api.dev.com",
} = {}) {
return { env, debug, apiUrl };
}
Null Coalescing Operator (??)
Provides a way to handle null/undefined values more precisely than logical OR.
// Null coalescing vs logical OR
const userInput = "";
const config = null;
// Logical OR (falsy check)
const value1 = userInput || "default"; // 'default' (empty string is falsy)
const value2 = config || { theme: "dark" }; // { theme: 'dark' }
// Null coalescing (nullish check)
const value3 = userInput ?? "default"; // '' (empty string is not nullish)
const value4 = config ?? { theme: "dark" }; // { theme: 'dark' }
// Practical examples
function processData(data) {
// Only use default if data is null/undefined, not if it's 0 or ''
const timeout = data.timeout ?? 5000;
const retries = data.retries ?? 0;
const message = data.message ?? "Processing...";
return { timeout, retries, message };
}
// Chaining with optional chaining
const user = { profile: null };
const theme = user.profile?.settings?.theme ?? "light";
// Assignment with null coalescing
let cache = null;
cache ??= new Map(); // Only assign if cache is null/undefined
cache.set("key", "value");
// Function parameters alternative
function createConnection(options = {}) {
const host = options.host ?? "localhost";
const port = options.port ?? 3000;
const ssl = options.ssl ?? false;
return { host, port, ssl };
}
Optional Chaining (?.)
Safely access nested object properties without explicit null checks.
// Basic optional chaining
const user = {
id: 1,
profile: {
name: "Alice",
settings: {
theme: "dark",
},
},
};
// Safe property access
console.log(user.profile?.name); // 'Alice'
console.log(user.profile?.email); // undefined
console.log(user.profile?.settings?.theme); // 'dark'
console.log(user.profile?.settings?.notifications?.email); // undefined
// Optional chaining with arrays
const users = [{ name: "Alice", contacts: { phone: "123-456" } }, { name: "Bob" }, null];
console.log(users[0]?.contacts?.phone); // '123-456'
console.log(users[1]?.contacts?.phone); // undefined
console.log(users[2]?.name); // undefined
// Optional method calling
const api = {
getData() {
return "data";
},
user: {
getName() {
return "Alice";
},
},
};
console.log(api.getData?.()); // 'data'
console.log(api.user?.getName?.()); // 'Alice'
console.log(api.user?.getEmail?.()); // undefined (method doesn't exist)
console.log(api.admin?.getName?.()); // undefined (object doesn't exist)
// Dynamic property access
const key = "settings";
console.log(user.profile?.[key]?.theme); // 'dark'
// Combining with null coalescing
const theme = user.profile?.settings?.theme ?? "light";
const email = user.profile?.contacts?.email ?? "no email provided";
// Real-world API example
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return {
name: data.profile?.name ?? "Unknown",
email: data.contacts?.primary?.email ?? null,
phone: data.contacts?.primary?.phone ?? null,
avatar: data.profile?.avatar?.url ?? "/default-avatar.png",
};
} catch (error) {
console.error("Failed to fetch user:", error);
return null;
}
}
Map Data Structure
Map provides key-value storage with any type of keys and maintains insertion order.
// Creating Maps
const userRoles = new Map();
const permissions = new Map([
['admin', ['read', 'write', 'delete']],
['user', ['read']],
['guest', []]
]);
// Basic Map operations
userRoles.set('john', 'admin');
userRoles.set('jane', 'user');
userRoles.set(1, 'guest'); // Number as key
userRoles.set(true, 'special'); // Boolean as key
console.log(userRoles.get('john')); // 'admin'
console.log(userRoles.has('jane')); // true
console.log(userRoles.size); // 4
// Objects as keys (major advantage over regular objects)
const user1 = { name: 'Alice' };
const user2 = { name: 'Bob' };
const userPreferences = new Map();
userPreferences.set(user1, { theme: 'dark', lang: 'en' });
userPreferences.set(user2, { theme: 'light', lang: 'es' });
console.log(userPreferences.get(user1)); // { theme: 'dark', lang: 'en' }
// Iteration methods
for (const [key, value] of userRoles) {
console.log(`${key}: ${value}`);
}
// Map methods
userRoles.forEach((value, key) => {
console.log(`User ${key} has role ${value}`);
});
console.log([...userRoles.keys()]); // All keys
console.log([...userRoles.values()]); // All values
console.log([...userRoles.entries()]); // All key-value pairs
// Practical use cases
// 1. Cache implementation
const cache = new Map();
function expensiveFunction(input) {
if (cache.has(input)) {
return cache.get(input);
}
const result = /* expensive computation */;
cache.set(input, result);
return result;
}
// 2. WeakMap for private data
const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this, { name, secrets: [] });
}
getName() {
return privateData.get(this).name;
}
addSecret(secret) {
privateData.get(this).secrets.push(secret);
}
}
Sets Data Structure
Set stores unique values of any type and maintains insertion order.
// Creating Sets
const uniqueNumbers = new Set([1, 2, 3, 2, 1]); // [1, 2, 3]
const tags = new Set();
// Basic Set operations
tags.add("javascript");
tags.add("react");
tags.add("nodejs");
tags.add("javascript"); // Duplicate ignored
console.log(tags.size); // 3
console.log(tags.has("react")); // true
// Set with objects
const users = new Set();
const user1 = { name: "Alice" };
const user2 = { name: "Bob" };
users.add(user1);
users.add(user2);
users.add(user1); // Duplicate object reference ignored
// Iteration
for (const tag of tags) {
console.log(tag);
}
tags.forEach((tag) => console.log(tag));
// Converting between Set and Array
const array = [1, 1, 2, 2, 3, 3];
const uniqueArray = [...new Set(array)]; // [1, 2, 3]
// Set operations (union, intersection, difference)
const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
// Union
const union = new Set([...set1, ...set2]); // [1, 2, 3, 4, 5, 6]
// Intersection
const intersection = new Set([...set1].filter((x) => set2.has(x))); // [3, 4]
// Difference
const difference = new Set([...set1].filter((x) => !set2.has(x))); // [1, 2]
// Practical use cases
// 1. Remove duplicates from array
function removeDuplicates(array) {
return [...new Set(array)];
}
// 2. Check if arrays have common elements
function hasCommonElements(arr1, arr2) {
const set1 = new Set(arr1);
return arr2.some((item) => set1.has(item));
}
// 3. WeakSet for object tracking
const processedObjects = new WeakSet();
function processObject(obj) {
if (processedObjects.has(obj)) {
return; // Already processed
}
// Process the object
processedObjects.add(obj);
}
Symbols
Unique identifiers that can be used as object property keys.
// Creating symbols
const sym1 = Symbol();
const sym2 = Symbol("description");
const sym3 = Symbol("description");
console.log(sym2 === sym3); // false (each symbol is unique)
console.log(sym2.toString()); // 'Symbol(description)'
// Symbols as object properties
const SECRET_KEY = Symbol("secretKey");
const user = {
name: "Alice",
age: 30,
[SECRET_KEY]: "top-secret-data",
};
console.log(user[SECRET_KEY]); // 'top-secret-data'
console.log(Object.keys(user)); // ['name', 'age'] (symbol not included)
// Global symbol registry
const globalSym1 = Symbol.for("myApp.userId");
const globalSym2 = Symbol.for("myApp.userId");
console.log(globalSym1 === globalSym2); // true
console.log(Symbol.keyFor(globalSym1)); // 'myApp.userId'
// Well-known symbols
const obj = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
},
[Symbol.toStringTag]: "CustomObject",
};
console.log([...obj]); // [1, 2, 3]
console.log(obj.toString()); // '[object CustomObject]'
// Private-like properties using symbols
class BankAccount {
constructor(balance) {
this.owner = "Public info";
this[Symbol.for("balance")] = balance; // "Private" balance
}
deposit(amount) {
this[Symbol.for("balance")] += amount;
}
getBalance() {
return this[Symbol.for("balance")];
}
}
const account = new BankAccount(1000);
console.log(account.owner); // Accessible
console.log(account.balance); // undefined
console.log(Object.keys(account)); // ['owner'] only
Generators and Iterators
Generators are functions that can pause and resume execution, perfect for creating custom iterators.
// Basic 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 and return
function* parameterizedGenerator(start) {
const received = yield start;
const final = yield start + received;
return final * 2;
}
const pGen = parameterizedGenerator(10);
console.log(pGen.next()); // { value: 10, done: false }
console.log(pGen.next(5)); // { value: 15, done: false }
console.log(pGen.next(3)); // { value: 6, done: true }
// Infinite generators
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const infinite = infiniteSequence();
console.log(infinite.next().value); // 0
console.log(infinite.next().value); // 1
// Fibonacci generator
function* fibonacci() {
let a = 0,
b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Custom iterable object
const range = {
start: 1,
end: 5,
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
},
};
console.log([...range]); // [1, 2, 3, 4, 5]
// Generator delegation
function* delegatingGenerator() {
yield* [1, 2, 3];
yield* numberGenerator();
yield "done";
}
console.log([...delegatingGenerator()]); // [1, 2, 3, 1, 2, 3, 'done']
// Practical use cases
// 1. Pagination
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield items.slice(i, i + pageSize);
}
}
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const page of paginate(items, 3)) {
console.log(page); // [1,2,3], [4,5,6], [7,8,9]
}
// 2. Async data processing
async function* fetchPages(urls) {
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
yield data;
}
}
Object Getters and Setters
Define computed properties and control access to object properties.
// Basic getter and setter
const user = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
},
get initials() {
return `${this.firstName[0]}${this.lastName[0]}`.toUpperCase();
},
};
console.log(user.fullName); // 'John Doe'
user.fullName = "Jane Smith";
console.log(user.firstName); // 'Jane'
console.log(user.initials); // 'JS'
// Class-based getters and setters
class Temperature {
constructor(celsius = 0) {
this._celsius = celsius;
}
get celsius() {
return this._celsius;
}
set celsius(value) {
if (value < -273.15) {
throw new Error("Temperature cannot be below absolute zero");
}
this._celsius = value;
}
get fahrenheit() {
return (this._celsius * 9) / 5 + 32;
}
set fahrenheit(value) {
this.celsius = ((value - 32) * 5) / 9;
}
get kelvin() {
return this._celsius + 273.15;
}
}
const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 86;
console.log(temp.celsius); // 30
// Using Object.defineProperty
const circle = {
_radius: 0,
};
Object.defineProperty(circle, "radius", {
get() {
return this._radius;
},
set(value) {
if (value < 0) throw new Error("Radius cannot be negative");
this._radius = value;
},
enumerable: true,
configurable: true,
});
Object.defineProperty(circle, "area", {
get() {
return Math.PI * this._radius ** 2;
},
enumerable: true,
configurable: false,
});
// Validation and transformation
class User {
constructor() {
this._email = "";
this._age = 0;
}
get email() {
return this._email;
}
set email(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error("Invalid email format");
}
this._email = value.toLowerCase();
}
get age() {
return this._age;
}
set age(value) {
if (value < 0 || value > 150) {
throw new Error("Age must be between 0 and 150");
}
this._age = Math.floor(value);
}
get isAdult() {
return this._age >= 18;
}
}
Function Context Methods: Bind, Call, Apply
Control the this context and how functions are invoked.
// Basic context demonstration
const person = {
name: "Alice",
greet() {
return `Hello, I'm ${this.name}`;
},
};
const anotherPerson = { name: "Bob" };
// Call method - invoke immediately with specified context
console.log(person.greet.call(anotherPerson)); // "Hello, I'm Bob"
// Apply method - like call but takes array of arguments
function introduce(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
console.log(introduce.call(person, "Hi", "!")); // "Hi, I'm Alice!"
console.log(introduce.apply(person, ["Hey", "."])); // "Hey, I'm Alice."
// Bind method - returns new function with bound context
const boundGreet = person.greet.bind(anotherPerson);
console.log(boundGreet()); // "Hello, I'm Bob"
// Practical examples
// 1. Event handlers with proper context
class Counter {
constructor() {
this.count = 0;
this.element = document.getElementById("counter");
// Bind to preserve context
this.increment = this.increment.bind(this);
this.element.addEventListener("click", this.increment);
}
increment() {
this.count++;
this.element.textContent = this.count;
}
}
// 2. Borrowing array methods
const arrayLike = {
0: "a",
1: "b",
2: "c",
length: 3,
};
// Use array methods on array-like objects
const realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // ['a', 'b', 'c']
// Modern alternative with spread
const modernArray = [...arrayLike]; // Doesn't work with array-like
const properArray = Array.from(arrayLike); // Better approach
// 3. Function currying with bind
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(4)); // 12
// 4. Method chaining with call/apply
function chainableMethods() {
const obj = {
value: 0,
add(n) {
this.value += n;
return this;
},
multiply(n) {
this.value *= n;
return this;
},
getValue() {
return this.value;
},
};
// Using call for method chaining
const result = obj.add.call(obj, 5).multiply.call(obj, 2).getValue();
return result; // 10
}
// 5. Partial application
function createLogger(level) {
return function (message) {
console.log(`[${level}] ${new Date().toISOString()}: ${message}`);
};
}
const errorLogger = createLogger("ERROR");
const infoLogger = createLogger("INFO");
// Advanced binding patterns
class DataProcessor {
constructor(name) {
this.name = name;
this.processors = new Map();
}
addProcessor(type, fn) {
// Bind processor functions to maintain context
this.processors.set(type, fn.bind(this));
}
process(type, data) {
const processor = this.processors.get(type);
if (!processor) {
throw new Error(`No processor for type: ${type}`);
}
return processor(data);
}
}