Advanced JavaScript Features

By Seokhyeon Byun 15 min read

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);
  }
}