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'
Interview Question: How does destructuring help with function parameters?
Answer: Destructuring makes functions more readable by clearly showing expected parameters, allows default values, and enables flexible parameter ordering without worrying about argument positions.
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: "https://api.example.com",
[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 };
}
Interview Question: What’s the difference between default parameters and checking for undefined in the function body?
Answer: Default parameters are evaluated at call time and only trigger for undefined
(not null
or falsy values). They provide cleaner syntax and better performance than manual checks.
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;
}
}
Interview Question: When should you use optional chaining vs traditional null checks?
Answer: Use optional chaining for deeply nested properties where intermediate values might be null/undefined. Use traditional null checks when you need to handle different falsy values distinctly or when you need custom error handling logic.
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);
}
Interview Question: When would you use a Map vs a regular object, and Set vs Array?
Answer:
- Map vs Object: Use Map when you need non-string keys, frequent additions/deletions, or when size matters. Objects are better for records with known string keys.
- Set vs Array: Use Set when uniqueness is required and you need fast lookups. Arrays are better when you need indexing, ordering operations, or duplicates.
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);
}
}
Interview Question: What’s the difference between call, apply, and bind?
Answer:
- call: Invokes function immediately with specified
this
and individual arguments - apply: Like call but takes arguments as an array
- bind: Returns new function with bound
this
and optional preset arguments, doesn’t invoke immediately