Skip to content

Instantly share code, notes, and snippets.

@carefree-ladka
Last active May 20, 2026 06:58
Show Gist options
  • Select an option

  • Save carefree-ladka/bd153e59cb9111ade43113401026158b to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/bd153e59cb9111ade43113401026158b to your computer and use it in GitHub Desktop.
Frontend Interview Guide

Frontend Interview Guide

Table of Contents

JavaScript Core

DOM & Browser

HTML

CSS

React

Next.js

System Design (Frontend)

API & Networking

Testing Basics


JavaScript Core

Variables: var, let, const

Theory:

  • var: function-scoped, hoisted, can be redeclared
  • let: block-scoped, hoisted but not initialized, cannot be redeclared
  • const: block-scoped, hoisted but not initialized, cannot be reassigned
// var - function scoped
function varExample() {
  var x = 1;
  if (true) {
    var x = 2; // same variable
    console.log(x); // 2
  }
  console.log(x); // 2
}

// let - block scoped
function letExample() {
  let x = 1;
  if (true) {
    let x = 2; // different variable
    console.log(x); // 2
  }
  console.log(x); // 1
}

// const - cannot reassign
const obj = { name: 'John' };
// obj = {}; // Error
obj.name = 'Jane'; // OK - mutating object is allowed

// Temporal Dead Zone
console.log(a); // undefined (var hoisting)
// console.log(b); // ReferenceError (TDZ)
var a = 1;
let b = 2;

Scope: global, function, block

Theory:

  • Global scope: accessible everywhere
  • Function scope: accessible within function
  • Block scope: accessible within {}
// Global scope
var globalVar = 'global';

function scopeExample() {
  // Function scope
  var functionVar = 'function';

  if (true) {
    // Block scope
    let blockVar = 'block';
    const blockConst = 'block const';
    var functionVar2 = 'function2'; // function scoped!

    console.log(globalVar); // 'global'
    console.log(functionVar); // 'function'
    console.log(blockVar); // 'block'
  }

  console.log(functionVar2); // 'function2'
  // console.log(blockVar); // ReferenceError
}

// Lexical scoping
function outer() {
  const outerVar = 'outer';

  function inner() {
    console.log(outerVar); // 'outer' - lexical scope
  }

  inner();
}

Hoisting

Theory: Variable and function declarations are moved to the top of their scope during compilation.

// Function hoisting
greet(); // 'Hello' - works!
function greet() {
  console.log('Hello');
}

// Variable hoisting
console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5

// Equivalent to:
var x;
console.log(x); // undefined
x = 5;

// let/const hoisting (TDZ)
// console.log(y); // ReferenceError
let y = 10;

// Function expression not hoisted
// sayHi(); // TypeError
var sayHi = function() {
  console.log('Hi');
};

// Class hoisting
// const p = new Person(); // ReferenceError
class Person {
  constructor(name) {
    this.name = name;
  }
}

Closures

Theory: A closure is a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned.

// Basic closure
function outer() {
  const message = 'Hello';

  function inner() {
    console.log(message); // accesses outer variable
  }

  return inner;
}

const closureFn = outer();
closureFn(); // 'Hello' - still has access!

// Practical example: Private variables
function createCounter() {
  let count = 0; // private variable

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined - private!

// Common mistake: Loop closure
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is function-scoped)

// Fix with let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 (let is block-scoped)

// Fix with IIFE
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// Prints: 0, 1, 2

Execution Context

Theory: Environment where JavaScript code is executed. Contains variable environment, scope chain, and this binding.

// Global Execution Context
var globalVar = 'global';

function executionExample() {
  // Function Execution Context
  var localVar = 'local';

  console.log(globalVar); // 'global'
  console.log(localVar); // 'local'
}

// Execution Context phases:
// 1. Creation Phase:
//    - Create scope chain
//    - Create variable object (hoisting)
//    - Determine 'this'
// 2. Execution Phase:
//    - Assign values
//    - Execute code

function contextPhases() {
  console.log(x); // undefined (creation phase)
  var x = 10;
  console.log(x); // 10 (execution phase)
}

Call Stack

Theory: LIFO data structure that tracks function execution. Each function call creates a new execution context pushed onto the stack.

function first() {
  console.log('First');
  second();
  console.log('First again');
}

function second() {
  console.log('Second');
  third();
  console.log('Second again');
}

function third() {
  console.log('Third');
}

first();
// Call Stack visualization:
// 1. first() pushed
// 2. second() pushed
// 3. third() pushed
// 4. third() popped (prints 'Third')
// 5. second() popped (prints 'Second again')
// 6. first() popped (prints 'First again')

// Stack overflow
function recursive() {
  recursive(); // infinite recursion
}
// recursive(); // RangeError: Maximum call stack size exceeded

Event Loop

Theory: Mechanism that handles asynchronous operations. Continuously checks call stack and task queues.

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

// Output: 1, 4, 3, 2
// Explanation:
// 1. Synchronous code executes first (1, 4)
// 2. Microtasks (Promises) execute (3)
// 3. Macrotasks (setTimeout) execute (2)

// Event Loop phases:
// 1. Execute synchronous code
// 2. Execute all microtasks
// 3. Execute one macrotask
// 4. Repeat

// Complex example
console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    setTimeout(() => console.log('Timeout in Promise 1'), 0);
  })
  .then(() => console.log('Promise 2'));

console.log('End');

// Output: Start, End, Promise 1, Promise 2, Timeout 1, Promise in Timeout 1, Timeout in Promise 1

this keyword

Theory: this refers to the object that is executing the current function. Its value depends on how the function is called.

// Global context
console.log(this); // Window (browser) or global (Node.js)

// Object method
const obj = {
  name: 'John',
  greet() {
    console.log(this.name);
  }
};
obj.greet(); // 'John'

// Function call
function showThis() {
  console.log(this);
}
showThis(); // Window (non-strict) or undefined (strict mode)

// Arrow functions (lexical this)
const obj2 = {
  name: 'Jane',
  greet: () => {
    console.log(this.name); // undefined - arrow function doesn't have own 'this'
  },
  greet2() {
    const inner = () => {
      console.log(this.name); // 'Jane' - inherits from greet2
    };
    inner();
  }
};

// Constructor function
function Person(name) {
  this.name = name;
}
const person = new Person('Alice');
console.log(person.name); // 'Alice'

// Event handler
document.getElementById('btn')?.addEventListener('click', function() {
  console.log(this); // button element
});

// Explicit binding
const obj3 = { name: 'Bob' };
function sayName() {
  console.log(this.name);
}
sayName.call(obj3); // 'Bob'
sayName.apply(obj3); // 'Bob'
const boundFn = sayName.bind(obj3);
boundFn(); // 'Bob'

Prototype and Prototype Chain

Theory: Every object has an internal [[Prototype]] property that references another object. Used for inheritance.

// Prototype basics
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const john = new Person('John');
john.greet(); // 'Hello, I'm John'

console.log(john.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

// Prototype chain
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of chain)

// Checking prototype chain
console.log(john instanceof Person); // true
console.log(john instanceof Object); // true
console.log(Person.prototype.isPrototypeOf(john)); // true

// Adding to prototype
Array.prototype.last = function() {
  return this[this.length - 1];
};
console.log([1, 2, 3].last()); // 3

// Object.create
const parent = {
  greet() {
    console.log('Hello from parent');
  }
};
const child = Object.create(parent);
child.greet(); // 'Hello from parent'
console.log(child.__proto__ === parent); // true

Inheritance

Theory: Mechanism where one object can inherit properties and methods from another.

// Prototypal inheritance
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

function Dog(name, breed) {
  Animal.call(this, name); // call parent constructor
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name} barks`);
};

const dog = new Dog('Rex', 'Labrador');
dog.speak(); // 'Rex makes a sound'
dog.bark(); // 'Rex barks'

// ES6 Class inheritance
class AnimalES6 {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class DogES6 extends AnimalES6 {
  constructor(name, breed) {
    super(name); // call parent constructor
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} barks`);
  }
}

const dogES6 = new DogES6('Max', 'Golden Retriever');
dogES6.speak(); // 'Max makes a sound'
dogES6.bark(); // 'Max barks'

// Multiple inheritance (mixin pattern)
const canEat = {
  eat() {
    console.log('Eating');
  }
};

const canWalk = {
  walk() {
    console.log('Walking');
  }
};

class Human {
  constructor(name) {
    this.name = name;
  }
}

Object.assign(Human.prototype, canEat, canWalk);

const human = new Human('Alice');
human.eat(); // 'Eating'
human.walk(); // 'Walking'

Functions: declaration, expression, arrow

Theory: Different ways to define functions with varying behaviors.

// Function Declaration
function add(a, b) {
  return a + b;
}
console.log(add(2, 3)); // 5

// Function Expression
const subtract = function(a, b) {
  return a - b;
};
console.log(subtract(5, 2)); // 3

// Arrow Function
const multiply = (a, b) => a * b;
console.log(multiply(3, 4)); // 12

// Arrow function variations
const single = x => x * 2; // single param, no parens
const noParams = () => console.log('Hi');
const multiLine = (a, b) => {
  const sum = a + b;
  return sum * 2;
};

// Differences:
// 1. 'this' binding
const obj = {
  name: 'Test',
  regular: function() {
    console.log(this.name); // 'Test'
  },
  arrow: () => {
    console.log(this.name); // undefined (lexical this)
  }
};

// 2. arguments object
function regularFunc() {
  console.log(arguments); // [1, 2, 3]
}
regularFunc(1, 2, 3);

const arrowFunc = () => {
  // console.log(arguments); // ReferenceError
};

// Use rest parameters instead
const arrowWithRest = (...args) => {
  console.log(args); // [1, 2, 3]
};
arrowWithRest(1, 2, 3);

// 3. Constructor
function RegularConstructor() {
  this.value = 42;
}
const instance = new RegularConstructor(); // OK

const ArrowConstructor = () => {
  this.value = 42;
};
// const instance2 = new ArrowConstructor(); // TypeError

// Named function expression
const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);
};
console.log(factorial(5)); // 120

Higher Order Functions

Theory: Functions that take functions as arguments or return functions.

// Function as argument
function higherOrder(callback) {
  callback();
}

higherOrder(() => console.log('Callback executed'));

// Function returning function
function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

// Common HOFs
const numbers = [1, 2, 3, 4, 5];

// map
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// reduce
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

// Composing functions
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const square = x => x * x;

const composed = compose(square, multiplyByTwo, addOne);
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64

// Pipe (left to right)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const piped = pipe(addOne, multiplyByTwo, square);
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64

Call, Apply, Bind

Theory: Methods to explicitly set this context and pass arguments.

const person = {
  name: 'John',
  greet(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
  }
};

const anotherPerson = { name: 'Jane' };

// call - arguments passed individually
person.greet.call(anotherPerson, 'Hello', '!');
// 'Hello, I'm Jane!'

// apply - arguments passed as array
person.greet.apply(anotherPerson, ['Hi', '.']);
// 'Hi, I'm Jane.'

// bind - returns new function with bound context
const boundGreet = person.greet.bind(anotherPerson);
boundGreet('Hey', '!!!');
// 'Hey, I'm Jane!!!'

// Partial application with bind
const greetJane = person.greet.bind(anotherPerson, 'Hello');
greetJane('!'); // 'Hello, I'm Jane!'

// Practical example: borrowing methods
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max); // 7

// Modern alternative with spread
console.log(Math.max(...numbers)); // 7

// Fixing 'this' in callbacks
const obj = {
  count: 0,
  increment() {
    this.count++;
  }
};

// Problem
// setTimeout(obj.increment, 100); // 'this' is lost

// Solution 1: bind
setTimeout(obj.increment.bind(obj), 100);

// Solution 2: arrow function
setTimeout(() => obj.increment(), 100);

// Solution 3: call
function executeCallback(callback, context) {
  callback.call(context);
}
executeCallback(obj.increment, obj);

Currying

Theory: Transforming a function with multiple arguments into a sequence of functions each taking a single argument.

// Regular function
function add(a, b, c) {
  return a + b + c;
}
console.log(add(1, 2, 3)); // 6

// Curried version
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
console.log(curriedAdd(1)(2)(3)); // 6

// Arrow function currying
const curriedAddArrow = a => b => c => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6

// Generic curry function
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}

const curriedSum = curry((a, b, c) => a + b + c);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6

// Practical example
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// Currying with map
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.map(double)); // [2, 4, 6, 8, 10]

// Partial application
const greet = greeting => name => `${greeting}, ${name}!`;
const sayHello = greet('Hello');
const sayHi = greet('Hi');

console.log(sayHello('John')); // 'Hello, John!'
console.log(sayHi('Jane')); // 'Hi, Jane!'

Debouncing and Throttling

Theory: Techniques to limit function execution frequency.

// Debouncing - execute after delay, reset timer on each call
function debounce(func, delay) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// Usage
const searchAPI = query => {
  console.log('Searching for:', query);
};

const debouncedSearch = debounce(searchAPI, 300);

// Only last call executes after 300ms
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc'); // Only this executes

// Throttling - execute at most once per interval
function throttle(func, limit) {
  let inThrottle;

  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;

      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// Usage
const handleScroll = () => {
  console.log('Scroll event');
};

const throttledScroll = throttle(handleScroll, 1000);

// Executes immediately, then at most once per second
window.addEventListener('scroll', throttledScroll);

// Advanced debounce with immediate option
function debounceAdvanced(func, delay, immediate = false) {
  let timeoutId;

  return function(...args) {
    const callNow = immediate && !timeoutId;

    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (!immediate) {
        func.apply(this, args);
      }
    }, delay);

    if (callNow) {
      func.apply(this, args);
    }
  };
}

// Practical React example
const SearchComponent = () => {
  const [query, setQuery] = useState('');

  const searchAPI = useCallback(
    debounce(q => {
      fetch(`/api/search?q=${q}`);
    }, 300),
    []
  );

  const handleChange = e => {
    setQuery(e.target.value);
    searchAPI(e.target.value);
  };

  return <input value={query} onChange={handleChange} />;
};

Promises

Theory: Object representing eventual completion or failure of an asynchronous operation.

// Creating a promise
const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve('Operation successful');
  } else {
    reject('Operation failed');
  }
});

// Consuming promises
promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Cleanup'));

// Promise states: pending, fulfilled, rejected

// Chaining promises
fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

// Promise.all - wait for all promises
const promise1 = Promise.resolve(3);
const promise2 = Promise.resolve(42);
const promise3 = new Promise(resolve => setTimeout(() => resolve('foo'), 100));

Promise.all([promise1, promise2, promise3])
  .then(values => console.log(values)); // [3, 42, 'foo']

// Promise.race - first to settle
Promise.race([
  new Promise(resolve => setTimeout(() => resolve('slow'), 500)),
  new Promise(resolve => setTimeout(() => resolve('fast'), 100))
]).then(result => console.log(result)); // 'fast'

// Promise.allSettled - wait for all, regardless of outcome
Promise.allSettled([
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3)
]).then(results => console.log(results));
// [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'error'}, {status: 'fulfilled', value: 3}]

// Promise.any - first fulfilled promise
Promise.any([
  Promise.reject('error1'),
  Promise.resolve('success'),
  Promise.resolve('success2')
]).then(result => console.log(result)); // 'success'

// Creating resolved/rejected promises
const resolved = Promise.resolve('value');
const rejected = Promise.reject('error');

// Promisifying callback-based functions
function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) reject(error);
        else resolve(result);
      });
    });
  };
}

Async/Await

Theory: Syntactic sugar over promises, making asynchronous code look synchronous.

// Basic async/await
async function fetchUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

// Error handling
async function fetchUserWithError() {
  try {
    const response = await fetch('/api/user');
    if (!response.ok) throw new Error('Failed to fetch');
    const user = await response.json();
    return user;
  } catch (error) {
    console.error(error);
    throw error;
  }
}

// Async functions always return a promise
async function getValue() {
  return 42;
}
getValue().then(value => console.log(value)); // 42

// Awaiting multiple promises
async function fetchMultiple() {
  // Sequential (slow)
  const user = await fetch('/api/user').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json());

  // Parallel (fast)
  const [user2, posts2] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);

  return { user2, posts2 };
}

// Top-level await (ES2022)
// const data = await fetch('/api/data');

// Async arrow functions
const asyncArrow = async () => {
  const result = await someAsyncOperation();
  return result;
};

// Async IIFE
(async () => {
  const data = await fetchData();
  console.log(data);
})();

// Error handling patterns
async function handleErrors() {
  // Pattern 1: try/catch
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error(error);
  }

  // Pattern 2: catch on promise
  const data2 = await fetchData().catch(error => {
    console.error(error);
    return null;
  });

  return data2;
}

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

Callback vs Promise vs Async/Await

Theory: Evolution of asynchronous programming patterns in JavaScript.

// 1. Callback pattern
function fetchDataCallback(callback) {
  setTimeout(() => {
    callback(null, { data: 'result' });
  }, 1000);
}

fetchDataCallback((error, result) => {
  if (error) {
    console.error(error);
  } else {
    console.log(result);
  }
});

// Callback hell
getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

// 2. Promise pattern
function fetchDataPromise() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ data: 'result' });
    }, 1000);
  });
}

fetchDataPromise()
  .then(result => console.log(result))
  .catch(error => console.error(error));

// Promise chaining (solves callback hell)
getData()
  .then(a => getMoreData(a))
  .then(b => getMoreData(b))
  .then(c => getMoreData(c))
  .then(d => console.log(d))
  .catch(error => console.error(error));

// 3. Async/Await pattern
async function fetchDataAsync() {
  try {
    const a = await getData();
    const b = await getMoreData(a);
    const c = await getMoreData(b);
    const d = await getMoreData(c);
    console.log(d);
  } catch (error) {
    console.error(error);
  }
}

// Comparison
// Callbacks: Hard to read, callback hell, error handling complex
// Promises: Better readability, chainable, standardized error handling
// Async/Await: Most readable, synchronous-looking code, easy error handling

// Converting between patterns
// Callback to Promise
function callbackToPromise(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) reject(error);
        else resolve(result);
      });
    });
  };
}

// Promise to Async/Await (just use await)
async function usePromise() {
  const result = await somePromise();
  return result;
}

Microtasks vs Macrotasks

Theory: Different queues for asynchronous operations with different priorities.

// Macrotasks: setTimeout, setInterval, setImmediate, I/O
// Microtasks: Promise.then, queueMicrotask, MutationObserver

console.log('1'); // Synchronous

setTimeout(() => {
  console.log('2'); // Macrotask
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // Microtask
});

queueMicrotask(() => {
  console.log('4'); // Microtask
});

console.log('5'); // Synchronous

// Output: 1, 5, 3, 4, 2
// Execution order:
// 1. All synchronous code
// 2. All microtasks
// 3. One macrotask
// 4. All microtasks created by that macrotask
// 5. Repeat from step 3

// Complex example
console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);

setTimeout(() => {
  console.log('Timeout 2');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    setTimeout(() => console.log('Timeout in Promise 1'), 0);
  })
  .then(() => console.log('Promise 2'));

console.log('End');

// Output:
// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Promise in Timeout 1
// Timeout 2
// Timeout in Promise 1

// Practical implications
async function processData() {
  console.log('Processing');

  // This runs in next microtask
  await Promise.resolve();
  console.log('After await');

  // This runs in next macrotask
  setTimeout(() => console.log('In timeout'), 0);
}

processData();
console.log('After function call');

// Output:
// Processing
// After function call
// After await
// In timeout

Shallow Copy vs Deep Copy

Theory: Shallow copy copies references, deep copy creates new objects recursively.

// Shallow copy methods
const original = {
  name: 'John',
  address: { city: 'NYC' }
};

// 1. Spread operator
const shallow1 = { ...original };
shallow1.name = 'Jane'; // doesn't affect original
shallow1.address.city = 'LA'; // AFFECTS original!

// 2. Object.assign
const shallow2 = Object.assign({}, original);

// 3. Array shallow copy
const arr = [1, 2, [3, 4]];
const shallowArr1 = [...arr];
const shallowArr2 = arr.slice();
const shallowArr3 = Array.from(arr);

shallowArr1[2][0] = 99; // affects original!

// Deep copy methods
// 1. JSON.parse/stringify (limitations: no functions, dates, undefined, symbols)
const deep1 = JSON.parse(JSON.stringify(original));
deep1.address.city = 'Boston'; // doesn't affect original

// Limitations
const objWithFunction = {
  name: 'John',
  greet() { console.log('Hi'); },
  date: new Date(),
  undef: undefined,
  sym: Symbol('test')
};

const copied = JSON.parse(JSON.stringify(objWithFunction));
// Lost: greet function, date becomes string, undefined removed, symbol removed

// 2. structuredClone (modern, handles more types)
const deep2 = structuredClone(original);
deep2.address.city = 'Chicago'; // doesn't affect original

// 3. Custom deep clone
function deepClone(obj, hash = new WeakMap()) {
  // Handle primitives and null
  if (obj === null || typeof obj !== 'object') return obj;

  // Handle circular references
  if (hash.has(obj)) return hash.get(obj);

  // Handle Date
  if (obj instanceof Date) return new Date(obj);

  // Handle Array
  if (Array.isArray(obj)) {
    const arrCopy = [];
    hash.set(obj, arrCopy);
    obj.forEach((item, index) => {
      arrCopy[index] = deepClone(item, hash);
    });
    return arrCopy;
  }

  // Handle Object
  const objCopy = {};
  hash.set(obj, objCopy);
  Object.keys(obj).forEach(key => {
    objCopy[key] = deepClone(obj[key], hash);
  });

  return objCopy;
}

// Circular reference example
const circular = { name: 'Test' };
circular.self = circular;
const cloned = deepClone(circular);
console.log(cloned.self === cloned); // true

// Comparison
const test = {
  a: 1,
  b: { c: 2 }
};

const shallow = { ...test };
const deep = deepClone(test);

test.b.c = 99;
console.log(shallow.b.c); // 99 (affected)
console.log(deep.b.c); // 2 (not affected)

Pass by Value vs Reference

Theory: Primitives are passed by value, objects are passed by reference.

// Primitives (pass by value)
let a = 10;
let b = a; // copy of value
b = 20;
console.log(a); // 10 (unchanged)
console.log(b); // 20

function modifyPrimitive(x) {
  x = 100;
}
let num = 50;
modifyPrimitive(num);
console.log(num); // 50 (unchanged)

// Objects (pass by reference)
let obj1 = { value: 10 };
let obj2 = obj1; // reference to same object
obj2.value = 20;
console.log(obj1.value); // 20 (changed!)
console.log(obj2.value); // 20

function modifyObject(obj) {
  obj.value = 100; // modifies original
}
let myObj = { value: 50 };
modifyObject(myObj);
console.log(myObj.value); // 100 (changed!)

// Reassignment vs mutation
function reassignObject(obj) {
  obj = { value: 200 }; // creates new object, doesn't affect original
}
let myObj2 = { value: 50 };
reassignObject(myObj2);
console.log(myObj2.value); // 50 (unchanged)

// Arrays (also pass by reference)
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4] (changed!)

function modifyArray(arr) {
  arr.push(5); // modifies original
}
modifyArray(arr1);
console.log(arr1); // [1, 2, 3, 4, 5]

function reassignArray(arr) {
  arr = [10, 20]; // doesn't affect original
}
reassignArray(arr1);
console.log(arr1); // [1, 2, 3, 4, 5] (unchanged)

// Avoiding mutations
function safeModify(obj) {
  const copy = { ...obj };
  copy.value = 100;
  return copy;
}

let original = { value: 50 };
let modified = safeModify(original);
console.log(original.value); // 50 (unchanged)
console.log(modified.value); // 100

// Comparison
let x = { a: 1 };
let y = { a: 1 };
console.log(x === y); // false (different references)

let z = x;
console.log(x === z); // true (same reference)

Type Coercion

Theory: Automatic or implicit conversion of values from one type to another.

// String coercion
console.log('5' + 3); // '53' (number to string)
console.log('5' + true); // '5true'
console.log('5' + null); // '5null'
console.log('5' + undefined); // '5undefined'

// Number coercion
console.log('5' - 3); // 2 (string to number)
console.log('5' * '2'); // 10
console.log('5' / '2'); // 2.5
console.log('5' % '2'); // 1

// Boolean coercion
console.log(Boolean('')); // false
console.log(Boolean('hello')); // true
console.log(Boolean(0)); // false
console.log(Boolean(42)); // true
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean([])); // true
console.log(Boolean({})); // true

// Falsy values: false, 0, '', null, undefined, NaN
// Everything else is truthy

// Explicit coercion
console.log(String(123)); // '123'
console.log(Number('123')); // 123
console.log(Number('123abc')); // NaN
console.log(parseInt('123abc')); // 123
console.log(parseFloat('123.45abc')); // 123.45

// Implicit coercion in conditions
if ('hello') console.log('truthy'); // executes
if (0) console.log('truthy'); // doesn't execute

// Coercion with operators
console.log(true + true); // 2
console.log(true + false); // 1
console.log([] + []); // '' (empty string)
console.log([] + {}); // '[object Object]'
console.log({} + []); // '[object Object]' or 0 (depends on context)

// Unary plus operator
console.log(+'42'); // 42
console.log(+true); // 1
console.log(+false); // 0
console.log(+null); // 0
console.log(+undefined); // NaN

// Double NOT operator (converts to boolean)
console.log(!!'hello'); // true
console.log(!!0); // false

// Common pitfalls
console.log(null == undefined); // true (coercion)
console.log(null === undefined); // false (no coercion)
console.log('0' == 0); // true (coercion)
console.log('0' === 0); // false (no coercion)
console.log(false == '0'); // true
console.log(false === '0'); // false

== vs ===

Theory: == performs type coercion, === checks type and value without coercion.

// === (strict equality)
console.log(5 === 5); // true
console.log(5 === '5'); // false (different types)
console.log(true === 1); // false
console.log(null === undefined); // false
console.log(NaN === NaN); // false (special case)

// == (loose equality with coercion)
console.log(5 == 5); // true
console.log(5 == '5'); // true (string coerced to number)
console.log(true == 1); // true
console.log(false == 0); // true
console.log(null == undefined); // true (special case)
console.log('' == 0); // true
console.log('0' == 0); // true
console.log(false == '0'); // true

// Comparison rules for ==
// 1. If types are same, compare like ===
// 2. null == undefined (and vice versa)
// 3. Number compared with string: convert string to number
// 4. Boolean: convert to number (true -> 1, false -> 0)
// 5. Object compared with primitive: convert object to primitive

// Object comparison
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;

console.log(obj1 == obj2); // false (different references)
console.log(obj1 === obj2); // false
console.log(obj1 == obj3); // true (same reference)
console.log(obj1 === obj3); // true

// Array comparison
console.log([] == []); // false (different references)
console.log([] == false); // true (coercion)
console.log([] == 0); // true
console.log([''] == ''); // true

// Best practices
// Always use === unless you specifically need coercion
// Use == only for null/undefined checks
if (value == null) {
  // checks for both null and undefined
}

// Equivalent to:
if (value === null || value === undefined) {
  // more explicit
}

// Special cases
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true

console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false

// Object.is() is more precise than ===

null vs undefined

Theory: null is intentional absence of value, undefined is uninitialized or missing value.

// undefined - variable declared but not assigned
let a;
console.log(a); // undefined
console.log(typeof a); // 'undefined'

// null - intentional absence of value
let b = null;
console.log(b); // null
console.log(typeof b); // 'object' (historical bug)

// When you get undefined
let x;
console.log(x); // undefined (uninitialized)

function test(param) {
  console.log(param); // undefined (missing argument)
}
test();

const obj = { name: 'John' };
console.log(obj.age); // undefined (missing property)

function noReturn() {}
console.log(noReturn()); // undefined (no return value)

// When you get null
const element = document.getElementById('nonexistent');
console.log(element); // null (not found)

const match = 'hello'.match(/\d+/);
console.log(match); // null (no match)

// Comparison
console.log(null == undefined); // true (loose equality)
console.log(null === undefined); // false (strict equality)

console.log(null == 0); // false
console.log(undefined == 0); // false

// Checking for null or undefined
function checkValue(value) {
  // Check for both
  if (value == null) {
    console.log('null or undefined');
  }

  // Check specifically
  if (value === null) {
    console.log('null');
  }

  if (value === undefined) {
    console.log('undefined');
  }

  // Check for falsy (includes null, undefined, 0, '', false)
  if (!value) {
    console.log('falsy');
  }
}

// Default values
function greet(name) {
  // Old way
  name = name || 'Guest';

  // Better way (handles 0, '', false correctly)
  name = name ?? 'Guest'; // nullish coalescing

  console.log(`Hello, ${name}`);
}

// Default parameters
function greet2(name = 'Guest') {
  console.log(`Hello, ${name}`);
}

// Optional chaining
const user = {
  name: 'John',
  address: null
};

console.log(user.address?.city); // undefined (no error)
console.log(user.contact?.phone); // undefined

// Setting to null vs deleting
const obj2 = { a: 1, b: 2 };
obj2.a = null; // property exists with null value
delete obj2.b; // property removed
console.log('a' in obj2); // true
console.log('b' in obj2); // false

map, filter, reduce

Theory: Array methods for transforming, filtering, and aggregating data.

const numbers = [1, 2, 3, 4, 5];

// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

const users = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' }
];
const names = users.map(user => user.name);
console.log(names); // ['John', 'Jane']

// map with index
const withIndex = numbers.map((n, index) => `${index}: ${n}`);
console.log(withIndex); // ['0: 1', '1: 2', '2: 3', '3: 4', '4: 5']

// filter - keep elements that pass test
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

const adults = [
  { name: 'John', age: 25 },
  { name: 'Jane', age: 17 },
  { name: 'Bob', age: 30 }
].filter(person => person.age >= 18);
console.log(adults); // [{ name: 'John', age: 25 }, { name: 'Bob', age: 30 }]

// reduce - aggregate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

const product = numbers.reduce((acc, n) => acc * n, 1);
console.log(product); // 120

// reduce for complex transformations
const items = [
  { category: 'fruit', name: 'apple' },
  { category: 'fruit', name: 'banana' },
  { category: 'vegetable', name: 'carrot' }
];

const grouped = items.reduce((acc, item) => {
  if (!acc[item.category]) {
    acc[item.category] = [];
  }
  acc[item.category].push(item.name);
  return acc;
}, {});
console.log(grouped);
// { fruit: ['apple', 'banana'], vegetable: ['carrot'] }

// Chaining methods
const result = numbers
  .filter(n => n % 2 === 0)
  .map(n => n * 2)
  .reduce((acc, n) => acc + n, 0);
console.log(result); // 12 (2*2 + 4*2 = 4 + 8)

// Practical example: counting occurrences
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, orange: 1 }

// Flattening arrays
const nested = [[1, 2], [3, 4], [5]];
const flattened = nested.reduce((acc, arr) => acc.concat(arr), []);
console.log(flattened); // [1, 2, 3, 4, 5]

// Modern alternative: flat()
console.log(nested.flat()); // [1, 2, 3, 4, 5]

// Implementing map with reduce
const customMap = (arr, fn) => {
  return arr.reduce((acc, item, index) => {
    acc.push(fn(item, index));
    return acc;
  }, []);
};

// Implementing filter with reduce
const customFilter = (arr, fn) => {
  return arr.reduce((acc, item, index) => {
    if (fn(item, index)) {
      acc.push(item);
    }
    return acc;
  }, []);
};

forEach, find, some, every

Theory: Array iteration and search methods.

const numbers = [1, 2, 3, 4, 5];

// forEach - iterate over each element (no return value)
numbers.forEach((n, index) => {
  console.log(`Index ${index}: ${n}`);
});

// Cannot break out of forEach
numbers.forEach(n => {
  if (n === 3) return; // only skips current iteration
  console.log(n);
});

// find - returns first element that matches
const found = numbers.find(n => n > 3);
console.log(found); // 4

const users = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' },
  { id: 3, name: 'Bob' }
];
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: 'Jane' }

// Returns undefined if not found
const notFound = numbers.find(n => n > 10);
console.log(notFound); // undefined

// findIndex - returns index of first match
const index = numbers.findIndex(n => n > 3);
console.log(index); // 3

// some - returns true if ANY element passes test
const hasEven = numbers.some(n => n % 2 === 0);
console.log(hasEven); // true

const hasNegative = numbers.some(n => n < 0);
console.log(hasNegative); // false

// every - returns true if ALL elements pass test
const allPositive = numbers.every(n => n > 0);
console.log(allPositive); // true

const allEven = numbers.every(n => n % 2 === 0);
console.log(allEven); // false

// Practical examples
const products = [
  { name: 'Laptop', price: 1000, inStock: true },
  { name: 'Phone', price: 500, inStock: false },
  { name: 'Tablet', price: 300, inStock: true }
];

// Check if any product is out of stock
const hasOutOfStock = products.some(p => !p.inStock);
console.log(hasOutOfStock); // true

// Check if all products are in stock
const allInStock = products.every(p => p.inStock);
console.log(allInStock); // false

// Find expensive product
const expensive = products.find(p => p.price > 800);
console.log(expensive); // { name: 'Laptop', ... }

// forEach vs map
// forEach: side effects, no return value
const result1 = numbers.forEach(n => n * 2);
console.log(result1); // undefined

// map: returns new array
const result2 = numbers.map(n => n * 2);
console.log(result2); // [2, 4, 6, 8, 10]

// Performance: some/every short-circuit
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);

// Stops at first match
const hasFive = largeArray.some(n => n === 5); // Fast

// Checks all elements
const allChecked = largeArray.forEach(n => n === 5); // Slower

Destructuring

Theory: Extract values from arrays or properties from objects into distinct variables.

// Array destructuring
const arr = [1, 2, 3, 4, 5];
const [first, second] = arr;
console.log(first, second); // 1, 2

// Skip elements
const [a, , c] = arr;
console.log(a, c); // 1, 3

// Rest operator
const [head, ...tail] = arr;
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

// Default values
const [x, y, z = 0] = [1, 2];
console.log(z); // 0

// Swapping variables
let p = 1, q = 2;
[p, q] = [q, p];
console.log(p, q); // 2, 1

// Object destructuring
const user = {
  name: 'John',
  age: 30,
  email: 'john@example.com'
};

const { name, age } = user;
console.log(name, age); // 'John', 30

// Rename variables
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // 'John', 30

// Default values
const { name: n, country = 'USA' } = user;
console.log(country); // 'USA'

// Nested destructuring
const person = {
  name: 'Jane',
  address: {
    city: 'NYC',
    zip: '10001'
  }
};

const { address: { city, zip } } = person;
console.log(city, zip); // 'NYC', '10001'

// Rest in objects
const { name: personName, ...rest } = user;
console.log(rest); // { age: 30, email: 'john@example.com' }

// Function parameters
function greet({ name, age }) {
  console.log(`Hello ${name}, you are ${age}`);
}
greet(user); // 'Hello John, you are 30'

// With defaults
function greet2({ name = 'Guest', age = 0 } = {}) {
  console.log(`Hello ${name}, you are ${age}`);
}
greet2(); // 'Hello Guest, you are 0'

// Array of objects
const users = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' }
];

users.forEach(({ id, name }) => {
  console.log(`${id}: ${name}`);
});

// Computed property names
const key = 'name';
const { [key]: value } = user;
console.log(value); // 'John'

// Practical React example
function UserProfile({ user: { name, email, avatar } }) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
}

Spread and Rest Operators

Theory: Spread (...) expands iterables, Rest (...) collects multiple elements.

// Spread operator

// Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

// Copy array
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] (unchanged)

// Objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }

// Override properties
const defaults = { theme: 'light', lang: 'en' };
const userPrefs = { theme: 'dark' };
const settings = { ...defaults, ...userPrefs };
console.log(settings); // { theme: 'dark', lang: 'en' }

// Function arguments
const numbers = [1, 2, 3];
console.log(Math.max(...numbers)); // 3

// String to array
const str = 'hello';
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

// Rest operator

// Function parameters
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Must be last parameter
function greet(greeting, ...names) {
  return `${greeting} ${names.join(', ')}`;
}
console.log(greet('Hello', 'John', 'Jane', 'Bob'));
// 'Hello John, Jane, Bob'

// Array destructuring
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]

// Object destructuring
const user = { name: 'John', age: 30, email: 'john@example.com' };
const { name, ...otherProps } = user;
console.log(name); // 'John'
console.log(otherProps); // { age: 30, email: 'john@example.com' }

// Practical examples

// Immutable array operations
const todos = ['Task 1', 'Task 2', 'Task 3'];

// Add item
const withNew = [...todos, 'Task 4'];

// Remove item
const withoutSecond = [...todos.slice(0, 1), ...todos.slice(2)];

// Update item
const updated = todos.map((todo, i) =>
  i === 1 ? 'Updated Task' : todo
);

// Immutable object updates
const state = {
  user: { name: 'John', age: 30 },
  theme: 'light'
};

// Update nested property
const newState = {
  ...state,
  user: {
    ...state.user,
    age: 31
  }
};

// React props spreading
function Button(props) {
  return <button {...props}>Click me</button>;
}

// Usage
<Button className="btn" onClick={handleClick} disabled={false} />

// Conditional spreading
const conditionalProps = {
  ...(isDisabled && { disabled: true }),
  ...(hasError && { className: 'error' })
};

Optional Chaining

Theory: Safely access nested object properties without checking each level.

// Without optional chaining
const user = {
  name: 'John',
  address: {
    city: 'NYC'
  }
};

// Verbose null checking
const city = user && user.address && user.address.city;
console.log(city); // 'NYC'

// With optional chaining
const city2 = user?.address?.city;
console.log(city2); // 'NYC'

// Returns undefined if any part is null/undefined
const zip = user?.address?.zip;
console.log(zip); // undefined (no error!)

// Accessing nested properties
const user2 = null;
console.log(user2?.address?.city); // undefined (no error)

// Optional chaining with arrays
const users = [
  { name: 'John', address: { city: 'NYC' } },
  { name: 'Jane' }
];

console.log(users[0]?.address?.city); // 'NYC'
console.log(users[1]?.address?.city); // undefined
console.log(users[2]?.address?.city); // undefined

// Optional chaining with function calls
const obj = {
  method() {
    return 'result';
  }
};

console.log(obj.method?.()); // 'result'
console.log(obj.nonExistent?.()); // undefined (no error)

// Dynamic property access
const propName = 'address';
console.log(user?.[propName]?.city); // 'NYC'

// Combining with nullish coalescing
const displayCity = user?.address?.city ?? 'Unknown';
console.log(displayCity); // 'NYC'

const displayZip = user?.address?.zip ?? 'N/A';
console.log(displayZip); // 'N/A'

// Practical examples

// API response handling
const response = {
  data: {
    user: {
      profile: {
        avatar: 'url'
      }
    }
  }
};

const avatar = response?.data?.user?.profile?.avatar ?? 'default.png';

// Event handling
document.getElementById('btn')?.addEventListener('click', () => {
  console.log('Clicked');
});

// React component
function UserProfile({ user }) {
  return (
    <div>
      <h2>{user?.name ?? 'Guest'}</h2>
      <p>{user?.email ?? 'No email'}</p>
      <img src={user?.avatar ?? 'default.png'} />
    </div>
  );
}

// Array methods
const items = null;
const firstItem = items?.[0];
const length = items?.length ?? 0;
const mapped = items?.map(x => x * 2) ?? [];

// Short-circuit evaluation
let count = 0;
const result = null?.method(count++);
console.log(count); // 0 (method not called)

// Cannot use on left side of assignment
// user?.address?.city = 'LA'; // SyntaxError

// Comparison with other operators
const value = null;

// Optional chaining
console.log(value?.prop); // undefined

// Logical AND
console.log(value && value.prop); // null

// Difference: optional chaining only checks null/undefined
// Logical AND checks for falsy values (0, '', false, etc.)
const obj2 = { count: 0 };
console.log(obj2?.count); // 0
console.log(obj2 && obj2.count); // 0
console.log(obj2.count ?? 'default'); // 0
console.log(obj2.count || 'default'); // 'default'

Modules: CommonJS vs ES Modules

Theory: Different module systems for organizing and sharing code.

// CommonJS (Node.js traditional)

// Exporting
// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

// Or
exports.add = add;
exports.subtract = subtract;

// Single export
module.exports = function multiply(a, b) {
  return a * b;
};

// Importing
const math = require('./math');
console.log(math.add(2, 3)); // 5

// Destructuring
const { add, subtract } = require('./math');
console.log(add(2, 3)); // 5

// ES Modules (modern JavaScript)

// Exporting
// math.mjs or math.js (with "type": "module" in package.json)
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// Default export
export default function multiply(a, b) {
  return a * b;
}

// Named + default export
export { add, subtract };
export default multiply;

// Importing
import multiply from './math.js';
import { add, subtract } from './math.js';
import multiply, { add, subtract } from './math.js';

// Rename imports
import { add as addition } from './math.js';

// Import all
import * as math from './math.js';
console.log(math.add(2, 3));

// Import for side effects only
import './setup.js';

// Dynamic imports
const math = await import('./math.js');
// Or
import('./math.js').then(math => {
  console.log(math.add(2, 3));
});

// Key differences

// 1. Syntax
// CommonJS: require/module.exports
// ES Modules: import/export

// 2. Loading
// CommonJS: synchronous, runtime
// ES Modules: asynchronous, compile-time

// 3. Tree shaking
// CommonJS: not supported
// ES Modules: supported (removes unused code)

// 4. Top-level await
// CommonJS: not supported
// ES Modules: supported

// 5. this context
// CommonJS: this === module.exports
// ES Modules: this === undefined

// 6. File extension
// CommonJS: .js, .cjs
// ES Modules: .mjs, .js (with package.json config)

// Interoperability

// Import CommonJS in ES Module
import pkg from './commonjs-module.js';
const { method } = pkg;

// Import ES Module in CommonJS (async)
(async () => {
  const { method } = await import('./es-module.mjs');
})();

// Practical examples

// utils.js (ES Module)
export const API_URL = 'https://api.example.com';

export function fetchData(endpoint) {
  return fetch(`${API_URL}/${endpoint}`);
}

export class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  get(endpoint) {
    return fetch(`${this.baseURL}/${endpoint}`);
  }
}

export default new ApiClient(API_URL);

// app.js
import apiClient, { fetchData, API_URL } from './utils.js';

// Conditional imports
let module;
if (condition) {
  module = await import('./module-a.js');
} else {
  module = await import('./module-b.js');
}

// Code splitting in React
const LazyComponent = React.lazy(() => import('./Component.js'));

Generators and Iterators

Theory: Generators are functions that can pause and resume execution. Iterators define custom iteration behavior.

// 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 }

// Using in for...of
for (const num of numberGenerator()) {
  console.log(num); // 1, 2, 3
}

// Infinite generator
function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const infinite = infiniteSequence();
console.log(infinite.next().value); // 0
console.log(infinite.next().value); // 1
console.log(infinite.next().value); // 2

// 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]

// Passing values to generator
function* twoWayGenerator() {
  const a = yield 'First';
  console.log('Received:', a);
  const b = yield 'Second';
  console.log('Received:', b);
}

const gen2 = twoWayGenerator();
console.log(gen2.next()); // { value: 'First', done: false }
console.log(gen2.next('Value A')); // Logs: Received: Value A
                                    // { value: 'Second', done: false }
console.log(gen2.next('Value B')); // Logs: Received: Value B
                                    // { value: undefined, done: true }

// Delegating generators
function* gen1() {
  yield 1;
  yield 2;
}

function* gen2() {
  yield* gen1();
  yield 3;
  yield 4;
}

console.log([...gen2()]); // [1, 2, 3, 4]

// Iterator protocol
const iterableObject = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;

    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const value of iterableObject) {
  console.log(value); // 1, 2, 3
}

// Practical examples

// 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]
}

// ID generator
function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const getId = idGenerator();
console.log(getId.next().value); // 1
console.log(getId.next().value); // 2

// Fibonacci sequence
function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5

// Async generator
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
  }
})();

// Tree traversal
function* traverseTree(node) {
  yield node.value;
  if (node.left) yield* traverseTree(node.left);
  if (node.right) yield* traverseTree(node.right);
}

const tree = {
  value: 1,
  left: { value: 2 },
  right: { value: 3 }
};

console.log([...traverseTree(tree)]); // [1, 2, 3]

Symbols

Theory: Unique and immutable primitive values, often used as object property keys.

// Creating symbols
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false (always unique)

// With description
const sym3 = Symbol('description');
console.log(sym3.toString()); // 'Symbol(description)'

// Symbol registry (shared symbols)
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true

// Get key from symbol
console.log(Symbol.keyFor(globalSym1)); // 'app.id'
console.log(Symbol.keyFor(sym1)); // undefined (not in registry)

// As object properties
const id = Symbol('id');
const user = {
  name: 'John',
  [id]: 123
};

console.log(user[id]); // 123
console.log(user.id); // undefined

// Symbols are hidden from normal iteration
console.log(Object.keys(user)); // ['name']
console.log(Object.getOwnPropertyNames(user)); // ['name']
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]

// Well-known symbols

// Symbol.iterator
const iterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => ({
        value: this.data[index++],
        done: index > this.data.length
      })
    };
  }
};

console.log([...iterable]); // [1, 2, 3]

// Symbol.toStringTag
class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyClass';
  }
}

console.log(Object.prototype.toString.call(new MyClass()));
// '[object MyClass]'

// Symbol.hasInstance
class MyArray {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof MyArray); // true

// Symbol.toPrimitive
const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 42;
    if (hint === 'string') return 'hello';
    return true;
  }
};

console.log(+obj); // 42
console.log(`${obj}`); // 'hello'
console.log(obj + ''); // 'true'

// Practical use cases

// Private properties (before # syntax)
const _private = Symbol('private');
class MyClass2 {
  constructor() {
    this[_private] = 'secret';
  }

  getPrivate() {
    return this[_private];
  }
}

const instance = new MyClass2();
console.log(instance.getPrivate()); // 'secret'
console.log(instance[_private]); // undefined (symbol not accessible)

// Enum-like values
const Colors = {
  RED: Symbol('red'),
  GREEN: Symbol('green'),
  BLUE: Symbol('blue')
};

function getColorName(color) {
  switch (color) {
    case Colors.RED: return 'Red';
    case Colors.GREEN: return 'Green';
    case Colors.BLUE: return 'Blue';
  }
}

// Metadata
const metadata = Symbol('metadata');
class User {
  constructor(name) {
    this.name = name;
    this[metadata] = {
      createdAt: new Date(),
      version: 1
    };
  }
}

// Avoiding property name collisions
const myLib = {
  [Symbol.for('myLib.version')]: '1.0.0'
};

const userCode = {
  version: '2.0.0', // doesn't conflict
  ...myLib
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment