Curated questions covering closures, hoisting, promises, async/await, event loop, ES6+, prototypes, DOM, and advanced JavaScript concepts.
A closure is a function that retains access to variables from its outer (enclosing) scope even after the outer function has returned. Used for data encapsulation, factory functions, and callbacks.
function counter() {
let count = 0;
return () => ++count;
}
const inc = counter();
console.log(inc()); // 1
console.log(inc()); // 2
Hoisting moves declarations to the top of their scope during compilation. var declarations are hoisted and initialized as undefined. Function declarations are fully hoisted. let and const are hoisted but not initialized (Temporal Dead Zone).
== performs type coercion before comparison ("5" == 5 is true). === compares both value and type without coercion ("5" === 5 is false). Always prefer === to avoid unexpected type coercion bugs.
The event loop allows JavaScript (single-threaded) to handle async operations. The call stack executes synchronous code. Async callbacks go to the task queue (macrotasks) or microtask queue (Promises). The event loop moves tasks to the call stack when it is empty. Microtasks run before macrotasks.
Both handle asynchronous operations. async/await is syntactic sugar over Promises, making async code look synchronous and easier to read. Error handling uses try/catch instead of .catch().
// Promise
fetch(url).then(r => r.json()).then(data => console.log(data)).catch(console.error);
// async/await
async function getData() {
try {
const r = await fetch(url);
const data = await r.json();
console.log(data);
} catch(e) { console.error(e); }
}
JavaScript objects have a prototype chain. When a property is not found on an object, JavaScript looks up the prototype chain. Objects inherit from Object.prototype by default. ES6 classes are syntactic sugar over prototypal inheritance.
function greet(greeting) { return `${greeting}, ${this.name}`; }
const user = { name: "Alice" };
greet.call(user, "Hello"); // "Hello, Alice"
greet.apply(user, ["Hi"]); // "Hi, Alice"
const sayHi = greet.bind(user);
sayHi("Hey"); // "Hey, Alice"
Event bubbling - events propagate from the target element up to the root (default). Event capturing - events propagate from root down to the target. Use addEventListener(event, handler, true) for capturing. stopPropagation() stops propagation.
undefined means a variable has been declared but not assigned a value. null is an intentional absence of value, explicitly assigned. typeof undefined is "undefined"; typeof null is "object" (a known JavaScript quirk).
const add = (a, b) => a + b;
const obj = {
name: "Alice",
greet: function() { return () => this.name; } // arrow inherits this
};
Destructuring extracts values from arrays or properties from objects into distinct variables.
const [a, b, ...rest] = [1, 2, 3, 4];
const { name, age = 25 } = { name: "Alice" };
function greet({ name, role = "user" }) { return `${name} is ${role}`; }
Spread (...) expands an iterable into individual elements. Rest (...) collects remaining arguments into an array.
const arr = [1, 2, 3];
const copy = [...arr, 4]; // [1,2,3,4]
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
A Promise represents an eventual value. States: pending (initial), fulfilled (resolved with value), rejected (failed with reason). Promises are immutable once settled. Use Promise.all(), Promise.race(), Promise.allSettled(), Promise.any() for multiple promises.
const nums = [1,2,3,4,5];
nums.map(x => x * 2); // [2,4,6,8,10]
nums.filter(x => x % 2 === 0); // [2,4]
nums.reduce((acc, x) => acc + x, 0); // 15
WeakMap and WeakSet hold weak references to objects - they do not prevent garbage collection. WeakMap keys must be objects. Neither is iterable. Useful for storing metadata about objects without memory leaks.
Synchronous code executes line by line, blocking until each operation completes. Asynchronous code initiates an operation and continues executing other code while waiting for the result (via callbacks, Promises, or async/await).
Generators are functions that can pause and resume execution using yield. They return an iterator. Useful for lazy evaluation, infinite sequences, and async flow control.
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
const gen = range(1, 3);
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
Both are Web Storage APIs. localStorage persists until explicitly cleared - survives browser restarts. sessionStorage is cleared when the browser tab is closed. Both store ~5MB of string data, are synchronous, and are never sent to the server.
Symbol is a primitive type that creates unique identifiers. Every Symbol() call returns a unique value. Used as object property keys to avoid name collisions, especially in libraries and frameworks.
const id = Symbol("id");
const user = { [id]: 42, name: "Alice" };
console.log(user[id]); // 42
console.log(Object.keys(user)); // ["name"] - Symbol not enumerable
const city = user?.address?.city;
const name = user.name ?? "Anonymous";
const count = data.count ?? 0; // 0 is valid - ?? will not replace it
Shallow copy copies only the top-level properties; nested objects are still referenced. Deep copy creates completely independent copies of all nested objects.
// Shallow
const copy = { ...original };
// Deep (modern, recommended)
const deep = structuredClone(original);
// Deep (legacy - loses functions/undefined)
const deep2 = JSON.parse(JSON.stringify(original));
Event delegation attaches a single event listener to a parent element instead of multiple listeners on children. It uses event bubbling - the parent catches events from children. More efficient for dynamic lists.
document.querySelector("#list").addEventListener("click", (e) => {
if (e.target.matches("li")) console.log(e.target.textContent);
});
Proxy wraps an object and intercepts fundamental operations (get, set, delete, etc.) via handler traps. Used for validation, logging, and reactive data systems (Vue 3 uses Proxy for reactivity).
const handler = {
get(target, key) {
return key in target ? target[key] : `${key} not found`;
},
set(target, key, value) {
if (typeof value !== "number") throw new TypeError("Numbers only");
target[key] = value;
return true;
}
};
const obj = new Proxy({}, handler);
const arr = [10, 20, 30];
for (const i in arr) console.log(i); // "0", "1", "2" (keys)
for (const v of arr) console.log(v); // 10, 20, 30 (values)
Memoization caches the results of expensive function calls and returns the cached result when the same inputs occur again. It trades memory for speed.
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const map = new Map();
map.set({ id: 1 }, "user");
map.set(42, "number key");
console.log(map.size); // 2
Set stores unique values of any type - duplicates are automatically ignored. Arrays allow duplicates. Set has O(1) lookup with has(); Array has O(n) with includes(). Use Set for deduplication and fast membership checks.
const set = new Set([1, 2, 2, 3, 3]);
console.log([...set]); // [1, 2, 3]
const unique = [...new Set(array)]; // fast deduplication
// ES Module
export const add = (a, b) => a + b;
import { add } from "./math.js";
// CommonJS
module.exports = { add };
const { add } = require("./math");
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
const results = await Promise.allSettled([p1, p2, p3]);
The TDZ is the period between entering a scope and the point where a let or const variable is declared. Accessing the variable in this zone throws a ReferenceError. var does not have a TDZ.
console.log(x); // ReferenceError: Cannot access before initialization
let x = 5;
console.log(y); // undefined (var has no TDZ)
var y = 5;
Currying transforms a function with multiple arguments into a sequence of functions each taking a single argument. Enables partial application and function composition.
const multiply = (a) => (b) => a * b;
const double = multiply(2);
console.log(double(5)); // 10
console.log(double(10)); // 20
const user = { name: "Alice", age: 30 };
Object.keys(user); // ["name", "age"]
Object.values(user); // ["Alice", 30]
Object.entries(user); // [["name","Alice"], ["age",30]]
const arr = [1, 2, 3, 4, 5];
arr.slice(1, 3); // [2, 3] - original unchanged
arr.splice(1, 2, 9, 9); // arr becomes [1,9,9,4,5]
Reflect is a built-in object that provides methods for interceptable JavaScript operations - the same operations that Proxy traps intercept. It makes it easier to call default behavior inside Proxy handlers.
const handler = {
set(target, key, value, receiver) {
console.log(`Setting ${key} = ${value}`);
return Reflect.set(target, key, value, receiver);
}
};
const obj = new Proxy({}, handler);
Inside an async function, throw and return Promise.reject() are equivalent - both cause the returned Promise to reject. Outside async functions, throw is synchronous and must be caught with try/catch synchronously.
async function fetchData() {
throw new Error("Failed"); // same as Promise.reject(new Error("Failed"))
}
try {
await fetchData();
} catch (e) {
console.error(e.message); // "Failed"
}
Array.from() converts any iterable or array-like object to an array. It accepts an optional mapping function as the second argument.
Array.from({ length: 5 }, (_, i) => i + 1); // [1,2,3,4,5]
Array.from("hello"); // ["h","e","l","l","o"]
Array.from(new Set([1,2,2,3])); // [1,2,3]
const merged = Object.assign({}, defaults, overrides); // mutates {}
const merged2 = { ...defaults, ...overrides }; // new object
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
// Output: 1, 4, 3, 2
el.textContent = userInput; // safe - no XSS risk
el.innerHTML = userInput; // DANGEROUS with untrusted input
Both schedule code to run asynchronously, but at different priority levels. Promise.resolve().then(fn) schedules a microtask which runs before the next macrotask. setTimeout(fn, 0) schedules a macrotask which runs after all microtasks are cleared.
AbortController allows you to cancel async operations like fetch requests. Create an AbortController, pass its signal to fetch, and call abort() to cancel.
const controller = new AbortController();
const { signal } = controller;
fetch("/api/data", { signal })
.then(r => r.json())
.catch(e => {
if (e.name === "AbortError") console.log("Request cancelled");
});
controller.abort(); // cancel the request
// Object.create
const animal = { speak() { return "..."; } };
const dog = Object.create(animal);
// Class
class Dog extends Animal { speak() { return "Woof"; } }
Array.isArray() is the reliable way to check if a value is an array. instanceof Array can fail across different iframes/realms because each frame has its own Array constructor. Array.isArray() works correctly in all contexts.
Array.isArray([1,2,3]); // true - always reliable
[1,2,3] instanceof Array; // true - but fails cross-realm
const date = new Date();
JSON.parse(JSON.stringify({ date })).date; // string, not Date
structuredClone({ date }).date; // proper Date object
// Declaration - hoisted
sayHello(); // works
function sayHello() { return "Hello"; }
// Expression - not hoisted
greet(); // TypeError: greet is not a function
var greet = function() { return "Hi"; };
Direct assignment creates a property that is writable, enumerable, and configurable by default. Object.defineProperty() gives full control over these descriptors.
Object.defineProperty(obj, "id", {
value: 42,
writable: false, // cannot reassign
enumerable: false, // hidden from for...in and Object.keys()
configurable: false // cannot delete or redefine
});
Both limit how often a function executes, but differently.
// Debounce: fires 300ms after user stops typing
const debounced = debounce((val) => search(val), 300);
// Throttle: fires at most once every 300ms while scrolling
const throttled = throttle(() => updatePosition(), 300);
// Inheritance
class Animal { speak() {} }
class Dog extends Animal { fetch() {} }
// Composition
const canSpeak = (state) => ({ speak: () => console.log(state.sound) });
const canFetch = (state) => ({ fetch: () => console.log("fetching") });
const createDog = (sound) => {
const state = { sound };
return { ...canSpeak(state), ...canFetch(state) };
};
Explore 500+ free tutorials across 20+ languages and frameworks.