A pedagogical Q&A guide for full-stack interviews. Each question has a plain-language explanation and a concrete code example. Read it once to understand, read it twice to recall in the room.
Answer:
JavaScript has seven primitive types: string, number, boolean, null, undefined, symbol, and bigint. Primitives are immutable values stored directly in the variable. Anything that is not a primitive is an object (arrays, functions, plain objects, dates, etc.).
typeof "hello"; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol("id"); // "symbol"
typeof 10n; // "bigint"
typeof null; // "object" (historical bug; null IS still a primitive)
typeof {}; // "object"Answer:
undefined means a variable was declared but never assigned, or a function returned nothing. null is an explicit assignment that says "no value here on purpose." Treat undefined as "JavaScript did this," and null as "I did this."
let a;
console.log(a); // undefined — not assigned
let b = null;
console.log(b); // null — explicitly empty
a == null; // true (both null and undefined match loosely)
a === null; // false (strict — they are different types)Answer:
== compares values after converting them to the same type (loose equality). === compares values without converting types (strict equality). Always prefer === unless you specifically want to treat null and undefined as equal.
0 == "0"; // true — string "0" is coerced to number 0
0 === "0"; // false — different types
null == undefined; // true
null === undefined; // falseAnswer:
Type coercion is JavaScript automatically converting one type to another. It happens with +, ==, template strings, and conditions. The rules are not always obvious, which is why strict equality is safer.
"5" + 2; // "52" — number coerced to string for +
"5" - 2; // 3 — string coerced to number for -
true + 1; // 2 — boolean to number
[] + []; // "" — both become empty strings
[] + {}; // "[object Object]"Answer:
NaN means "Not a Number" but is technically of type number. It is the result of invalid math like 0/0 or Number("abc"). NaN is never equal to anything, even itself, so === does not work. Use Number.isNaN().
const x = Number("abc");
x === NaN; // false — NaN is never equal to anything
Number.isNaN(x); // true — correct check
isNaN("abc"); // true but unsafe — coerces firstAnswer:
var is function-scoped and hoisted with an undefined initial value. let and const are block-scoped and live in the Temporal Dead Zone until declared. const cannot be reassigned, but the object it points to can still be mutated.
function demo() {
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1 — var leaks out of the block
// console.log(b); // ReferenceError
// console.log(c); // ReferenceError
}
const list = [1, 2];
list.push(3); // OK — mutating, not reassigning
// list = []; // TypeErrorAnswer:
The Temporal Dead Zone is the period from when a let or const variable enters scope until its declaration line runs. Accessing it during this window throws a ReferenceError. This forces you to declare before use.
// console.log(x); // ReferenceError — TDZ
let x = 5;
console.log(y); // undefined — var is hoisted with undefined
var y = 5;Answer:
JavaScript is always pass-by-value, but for objects the "value" is a reference to the object. So if you reassign the parameter, the caller does not see it; if you mutate the object, the caller does.
function reassign(obj) { obj = { name: "B" }; }
function mutate(obj) { obj.name = "B"; }
const user = { name: "A" };
reassign(user);
console.log(user.name); // "A" — reassignment didn't affect caller
mutate(user);
console.log(user.name); // "B" — mutation didAnswer:
Hoisting is JavaScript moving declarations to the top of their scope before execution. Function declarations are hoisted with their body. var declarations are hoisted but initialised as undefined. let and const are hoisted but stay uninitialised in the TDZ.
sayHi(); // "Hi" — function declaration is fully hoisted
function sayHi() { console.log("Hi"); }
console.log(a); // undefined — var hoisted, value is not
var a = 1;
// sayBye(); // TypeError — function expression not hoisted
var sayBye = function () { console.log("Bye"); };Answer:
Function scope means a variable is visible inside the entire function. Block scope limits visibility to the nearest { } block. var uses function scope; let and const use block scope.
function test() {
if (true) {
var x = 1; // function-scoped — visible below
let y = 2; // block-scoped — only visible in if-block
}
console.log(x); // 1
// console.log(y); // ReferenceError
}Answer:
A closure is a function that remembers variables from the scope where it was created, even after that scope has finished. Closures are used for data privacy, factory functions, and event handlers.
function counter() {
let count = 0; // captured by the inner function
return function () {
count++;
return count;
};
}
const next = counter();
next(); // 1
next(); // 2
next(); // 3 — count survives between callsAnswer:
If you use var inside a loop and create functions that reference the loop variable, all functions share the same variable. By the time they run, the loop has finished and the variable holds the final value. Use let (block-scoped per iteration) instead.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 0, 1, 2
}Answer:
Lexical scoping means a function's scope is determined by where it is written in the source code, not where it is called. A function can always see variables in the file or function it was defined inside.
const name = "outer";
function outer() {
const name = "inner";
function inner() { console.log(name); } // sees "inner"
return inner;
}
outer()(); // "inner"Answer:
An Immediately Invoked Function Expression runs the moment it is defined. Before ES6 modules, it was the standard way to create a private scope so variables didn't leak globally.
(function () {
const secret = 42;
console.log("runs immediately:", secret);
})();
// secret is not accessible hereAnswer:
this depends on how a function is called, not where it is defined. The rules in priority order: new binds this to the new instance; explicit call/apply/bind sets it; method calls set this to the object before the dot; otherwise it is undefined in strict mode or the global object otherwise.
const user = {
name: "Iqbal",
greet() { console.log(this.name); }
};
user.greet(); // "Iqbal" — method call
const fn = user.greet;
fn(); // undefined — plain call (strict mode)
fn.call({ name: "Bob" }); // "Bob" — explicitAnswer:
All three set this for a function. call(thisArg, a, b) invokes immediately with separate arguments. apply(thisArg, [a, b]) invokes immediately with an array of arguments. bind(thisArg, ...) returns a new function with this permanently set, without invoking it yet.
function greet(greeting, punct) {
return `${greeting}, ${this.name}${punct}`;
}
const u = { name: "Iqbal" };
greet.call(u, "Hi", "!"); // "Hi, Iqbal!"
greet.apply(u, ["Hi", "!"]); // "Hi, Iqbal!"
const bound = greet.bind(u, "Hi");
bound("!"); // "Hi, Iqbal!"Answer:
Arrow functions do not have their own this, arguments, or prototype. They inherit this from the surrounding scope. They cannot be used as constructors. Use them for short callbacks; use regular functions for object methods and constructors.
const obj = {
name: "X",
arrow: () => console.log(this.name), // this is NOT obj
normal() { console.log(this.name); } // this IS obj
};
obj.arrow(); // undefined (or global)
obj.normal(); // "X"Answer:
When a method is passed as a callback, this is lost because the function is no longer called as a method. Fix by binding or by using an arrow wrapper.
class Timer {
constructor() { this.seconds = 0; }
tick() { this.seconds++; console.log(this.seconds); }
}
const t = new Timer();
setInterval(t.tick, 1000); // breaks — this is undefined
setInterval(t.tick.bind(t), 1000); // works
setInterval(() => t.tick(), 1000); // also worksAnswer:
Every object has an internal link to another object called its prototype. When you read a property, JavaScript searches the object, then its prototype, then its prototype's prototype, until it reaches null. This is how inheritance works.
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.barks); // true — own property
console.log(dog.eats); // true — found via prototype
console.log(dog.flies); // undefined — end of chainAnswer:
prototype is a property on constructor functions. It is the object that will become the prototype of instances created with new. __proto__ (or Object.getPrototypeOf) is the actual prototype link on a created object.
function Dog() {}
Dog.prototype.bark = function () { return "woof"; };
const rex = new Dog();
rex.__proto__ === Dog.prototype; // true
rex.bark(); // "woof"Answer:
Use extends to inherit and super to call the parent constructor or methods. Under the hood, classes are still prototype-based.
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
speak() { return `${super.speak()} — woof`; }
}
new Dog("Rex").speak(); // "Rex makes a sound — woof"Answer:
static methods belong to the class itself, not instances — call them as ClassName.method(). Private fields use a # prefix and are inaccessible outside the class.
class Counter {
#count = 0;
increment() { this.#count++; }
get value() { return this.#count; }
static create() { return new Counter(); }
}
const c = Counter.create();
c.increment();
c.value; // 1
// c.#count; // SyntaxError — privateAnswer:
Object.create(proto) creates a new object with proto as its prototype, without running any constructor. new Constructor() creates an object whose prototype is Constructor.prototype and runs the constructor function.
const proto = { greet() { return "hi"; } };
const a = Object.create(proto);
a.greet(); // "hi"
function User() { this.name = "Iqbal"; }
const b = new User();
b.name; // "Iqbal"Answer:
new creates an empty object, links its prototype to the constructor's prototype, calls the constructor with this set to the new object, and returns the object (unless the constructor explicitly returns another object).
function newOperator(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const result = Ctor.apply(obj, args);
return (result && typeof result === "object") ? result : obj;
}
function Person(name) { this.name = name; }
const p = newOperator(Person, "Iqbal");
p.name; // "Iqbal"Answer:
A Promise represents a value that will exist later. It has three states: pending, fulfilled (resolved with a value), or rejected (with an error). You handle it with .then, .catch, .finally, or with await.
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), 100);
});
p.then(value => console.log(value)) // "done"
.catch(err => console.error(err))
.finally(() => console.log("cleanup"));Answer:
Wrapping an existing Promise in new Promise is called the "Promise constructor antipattern." It loses error context and is redundant. Just return the Promise directly.
// Bad
function getUser(id) {
return new Promise((resolve, reject) => {
fetch(`/api/users/${id}`).then(resolve, reject);
});
}
// Good
function getUser(id) {
return fetch(`/api/users/${id}`);
}Answer:
all waits for all to fulfill, fails fast on first rejection. allSettled waits for everyone, never rejects. race resolves or rejects with whichever finishes first. any resolves with the first fulfilled, ignoring rejections.
const p1 = Promise.resolve(1);
const p2 = Promise.reject("err");
const p3 = Promise.resolve(3);
await Promise.all([p1, p3]); // [1, 3]
await Promise.allSettled([p1, p2]); // [{status:"fulfilled",value:1}, {status:"rejected",reason:"err"}]
await Promise.race([p1, p2]); // 1
await Promise.any([p2, p3]); // 3Answer:
async makes a function return a Promise. await pauses execution inside the function until the Promise settles, then resumes with the resolved value (or throws on rejection). It is just sugar over .then.
async function load() {
try {
const res = await fetch("/api/data");
const json = await res.json();
return json;
} catch (e) {
console.error("failed:", e);
}
}Answer:
Awaiting one at a time runs them sequentially. To run in parallel, start all the Promises first, then await them — usually with Promise.all.
// Sequential — slow
const a = await fetchA();
const b = await fetchB();
// Parallel — faster, independent calls run together
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);Answer:
Run only N tasks at a time so you don't overwhelm a server or hit rate limits. A simple pattern is a worker pool that pulls from a queue.
async function pool(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then(r => { executing.delete(p); return r; });
results.push(p);
executing.add(p);
if (executing.size >= limit) await Promise.race(executing);
}
return Promise.all(results);
}
await pool(urls.map(u => () => fetch(u)), 3);Answer:
If a Promise rejects and nobody handles it, Node logs a warning and (in newer versions) exits the process. Browsers fire an unhandledrejection event. Always attach .catch or wrap await in try/catch.
process.on("unhandledRejection", (reason) => {
console.error("Unhandled:", reason);
});
window.addEventListener("unhandledrejection", e => {
console.error("Unhandled:", e.reason);
});Answer:
sleep returns a Promise that resolves after a delay. A timeout wrapper races a Promise against a timer that rejects.
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), ms)
);
return Promise.race([promise, timeout]);
}
await sleep(500);
await withTimeout(fetch("/slow"), 2000);Answer:
AbortController exposes a signal you can pass to cancellable APIs (fetch, timers, custom code). Calling controller.abort() triggers cancellation.
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
const res = await fetch("/slow", { signal: controller.signal });
} catch (e) {
if (e.name === "AbortError") console.log("cancelled");
}Answer:
JavaScript is single-threaded. The event loop pulls tasks from a queue and runs them on the call stack. Microtasks (Promises, queueMicrotask) run after the current task and before the next macrotask (setTimeout, I/O, UI events). The loop alternates: run one task, drain microtasks, render, repeat.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Order: 1, 4, 3, 2 — microtasks run before the next macrotaskAnswer:
Microtasks (Promise callbacks) run before macrotasks (setTimeout). Within microtasks, they run in queue order.
setTimeout(() => console.log("a"), 0);
Promise.resolve().then(() => console.log("b"));
Promise.resolve().then(() => console.log("c"));
console.log("d");
// d, b, c, aAnswer:
If microtasks keep scheduling more microtasks, the event loop never reaches macrotasks like timers or rendering. The page can freeze even though no single task is slow.
function starve() {
Promise.resolve().then(starve); // schedules itself forever
}
starve();
// setTimeout below never runs
setTimeout(() => console.log("never"), 0);Answer:
process.nextTick runs before any I/O or timers — even before other microtasks. setImmediate runs in the check phase, after I/O. setTimeout(fn, 0) runs in the timers phase, with at least 1ms delay.
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
// nextTick, promise, then timeout/immediate (order varies)Answer:
An iterator is an object with a next() method that returns { value, done }. Anything that implements [Symbol.iterator] can be iterated with for...of or spread.
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let i = this.from;
const last = this.to;
return {
next: () => i <= last
? { value: i++, done: false }
: { value: undefined, done: true }
};
}
};
for (const n of range) console.log(n); // 1, 2, 3Answer:
A generator is a function that can pause and resume. Declared with function* and uses yield. Calling it returns an iterator. Each next() runs until the next yield.
function* count() {
yield 1;
yield 2;
yield 3;
}
const g = count();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }Answer:
A Symbol is a unique, immutable primitive. Two symbols created with the same description are still different. They are commonly used as object keys that won't collide and as well-known protocol keys like Symbol.iterator.
const a = Symbol("id");
const b = Symbol("id");
a === b; // false
const user = { [a]: 123 };
user[a]; // 123Answer:
Async iterators yield Promises and are consumed with for await...of. They are perfect for streams of asynchronous data.
async function* lines(file) {
for (const chunk of file) {
for (const line of chunk.split("\n")) yield line;
}
}
for await (const line of lines(stream)) {
console.log(line);
}Answer:
A Map accepts any key (including objects), preserves insertion order, has a size property, and is optimised for frequent additions and lookups. An object only allows string/symbol keys and inherits from Object.prototype (which can collide with keys like toString).
const m = new Map();
const key = { id: 1 };
m.set(key, "value");
m.get(key); // "value"
m.size; // 1
const o = {};
o[key] = "x"; // key becomes "[object Object]"Answer:
A Set stores unique values with O(1) membership checks. Arrays allow duplicates and use O(n) indexOf. Use Set to deduplicate and to test "have I seen this?"
const seen = new Set();
const items = [1, 2, 2, 3, 1];
for (const x of items) seen.add(x);
[...seen]; // [1, 2, 3]Answer:
WeakMap and WeakSet hold their keys/values weakly — when no other reference exists, the entry can be garbage collected. Keys must be objects. Useful for attaching metadata to objects without preventing cleanup.
const cache = new WeakMap();
function meta(obj) {
if (!cache.has(obj)) cache.set(obj, { hits: 0 });
const entry = cache.get(obj);
entry.hits++;
return entry;
}
// When obj has no other refs, the cache entry disappears too.Answer:
map transforms each item. filter keeps items matching a predicate. reduce collapses to a single value. find returns the first match. some/every test conditions. None of these mutate the original array.
const nums = [1, 2, 3, 4];
nums.map(n => n * 2); // [2, 4, 6, 8]
nums.filter(n => n % 2 === 0); // [2, 4]
nums.reduce((s, n) => s + n, 0); // 10
nums.find(n => n > 2); // 3
nums.some(n => n > 3); // true
nums.every(n => n > 0); // trueAnswer:
Destructuring extracts properties from objects and elements from arrays into variables. You can rename, default, and nest.
const user = { name: "Iqbal", age: 25, address: { city: "Dhaka" } };
const { name, age: years, address: { city }, role = "guest" } = user;
// name="Iqbal", years=25, city="Dhaka", role="guest"
const [first, , third] = [1, 2, 3]; // skip middleAnswer:
They look the same (...) but have opposite jobs. Spread expands an iterable into individual elements. Rest collects multiple elements into one variable.
// Spread — expand
const arr = [1, 2, 3];
console.log(Math.max(...arr)); // 3
const merged = [...arr, 4, 5];
// Rest — collect
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }
sum(1, 2, 3); // 6Answer:
A shallow copy duplicates the top level but shares nested objects. A deep copy duplicates everything recursively. Modern JavaScript has structuredClone for true deep copies.
const original = { a: 1, nested: { b: 2 } };
const shallow = { ...original };
shallow.nested.b = 99;
original.nested.b; // 99 — same reference
const deep = structuredClone(original);
deep.nested.b = 5;
original.nested.b; // unchangedAnswer:
ESM (import/export) is the modern standard. It is static (analysed at parse time), supports tree-shaking, and runs asynchronously. CommonJS (require/module.exports) is Node's older system, dynamic, and synchronous. Browsers only support ESM.
// ESM
export const greet = name => `Hi ${name}`;
import { greet } from "./util.js";
// CommonJS
module.exports.greet = name => `Hi ${name}`;
const { greet } = require("./util");Answer:
When you import a value from an ES module, you get a live reference, not a copy. If the source module updates the variable, your import sees the new value. CommonJS exports a snapshot.
// counter.js
export let count = 0;
export const inc = () => count++;
// main.js
import { count, inc } from "./counter.js";
console.log(count); // 0
inc();
console.log(count); // 1 — live bindingAnswer:
import() is a function-like call that returns a Promise resolving to a module. It enables lazy loading and conditional imports.
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js");
renderChart();
});Answer:
Currying transforms a function with multiple arguments into a chain of functions, each taking one argument. Useful for partial application.
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6
const add5 = add(2)(3);
add5(10); // 15Answer:
Debounce postpones a function until events stop firing for delay ms (good for search-as-you-type). Throttle limits a function to run at most once every delay ms (good for scroll/resize handlers).
function debounce(fn, delay) {
let id;
return (...args) => {
clearTimeout(id);
id = setTimeout(() => fn(...args), delay);
};
}
function throttle(fn, delay) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn(...args);
}
};
}Answer:
Memoization caches the result of a function for a given input so repeated calls return instantly. Best for pure, expensive functions.
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const square = n => n * n;
const fastSquare = memoize(square);
fastSquare(5); // computed
fastSquare(5); // cachedAnswer:
Object.freeze makes an object's top level immutable — no adding, removing, or changing existing properties. It is shallow; nested objects can still mutate.
const config = Object.freeze({ env: "prod", limits: { rps: 10 } });
config.env = "dev"; // silently ignored (or throws in strict mode)
config.limits.rps = 100; // works — nested is not frozenAnswer:
get and set define properties that look like fields but run code. Useful for computed values, validation, and lazy loading.
class Temp {
constructor(c) { this._c = c; }
get fahrenheit() { return this._c * 9 / 5 + 32; }
set fahrenheit(f) { this._c = (f - 32) * 5 / 9; }
}
const t = new Temp(0);
t.fahrenheit; // 32
t.fahrenheit = 212;
t._c; // 100Answer:
A Proxy wraps an object and intercepts operations like get, set, has, and deleteProperty. Useful for validation, observation, and lazy data fetching.
const target = { count: 0 };
const proxy = new Proxy(target, {
set(obj, key, value) {
if (typeof value !== "number") throw new TypeError("number only");
obj[key] = value;
return true;
}
});
proxy.count = 5; // OK
// proxy.count = "x"; // TypeErrorAnswer:
Instead of attaching a listener to every child, attach one to a common ancestor and detect the source via event.target. Saves memory and works for elements added later.
document.querySelector("#list").addEventListener("click", (e) => {
const item = e.target.closest("li");
if (!item) return;
console.log("clicked:", item.dataset.id);
});Answer:
DOM events go through three phases: capturing (top-down to target), target, then bubbling (target back up). Most listeners run on bubble. Pass { capture: true } for capture phase.
parent.addEventListener("click", () => console.log("parent"), true); // capture
child.addEventListener("click", () => console.log("child")); // bubble
// Click on child logs: "parent" then "child"Answer:
localStorage persists per-origin, no expiry, ~5MB, synchronous. sessionStorage clears on tab close. Cookies are sent with HTTP requests automatically (~4KB). IndexedDB is a transactional, asynchronous database for large structured data.
localStorage.setItem("token", "abc");
sessionStorage.setItem("nav", "open");
document.cookie = "user=iqbal; max-age=3600; path=/";
// IndexedDB — async
const req = indexedDB.open("appdb", 1);Answer:
fetch returns a Promise, supports streams, and has a clean API. XMLHttpRequest is the older, callback-based API. fetch does NOT reject on HTTP errors — only network failures — so you must check response.ok.
const res = await fetch("/api/user");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();Answer:
A Web Worker runs JavaScript on a background thread, freeing the main thread. Workers cannot touch the DOM. Use them for CPU-heavy work like parsing or image processing.
// main.js
const worker = new Worker("worker.js");
worker.postMessage({ n: 1_000_000 });
worker.onmessage = (e) => console.log("result:", e.data);
// worker.js
onmessage = (e) => {
let sum = 0;
for (let i = 0; i < e.data.n; i++) sum += i;
postMessage(sum);
};Answer:
A Service Worker is a proxy between your page and the network. It can cache assets, intercept fetches, and enable offline mode and push notifications.
// register
navigator.serviceWorker.register("/sw.js");
// sw.js
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then(c => c || fetch(event.request))
);
});Answer:
requestAnimationFrame schedules a callback before the next browser repaint, typically 60 times per second. Use it for smooth animations instead of setTimeout.
function animate() {
ball.style.left = (parseInt(ball.style.left) + 2) + "px";
requestAnimationFrame(animate);
}
animate();Answer:
IntersectionObserver reports when an element enters or leaves the viewport. Used for lazy-loading images, infinite scroll, and analytics impressions.
const obs = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.src = e.target.dataset.src;
obs.unobserve(e.target);
}
}
});
document.querySelectorAll("img[data-src]").forEach(img => obs.observe(img));Answer:
Cross-Site Scripting injects attacker-controlled HTML or JavaScript into your page. Prevention: never insert raw user input as HTML, escape on output, prefer textContent for plain text, set a strong CSP, and validate URLs to block javascript: schemes. If you must render HTML, run it through a vetted sanitizer.
// BAD — uses raw HTML insertion
div.insertAdjacentHTML("beforeend", userInput);
// GOOD — text-only, no HTML parsing
div.textContent = userInput;Answer:
Content Security Policy is an HTTP header that tells the browser which scripts, styles, and resources are allowed. A strict CSP blocks inline scripts and unknown origins, killing most XSS vectors.
Content-Security-Policy:
default-src 'self';
script-src 'self';
img-src 'self' https://cdn.example.com;
object-src 'none';
Answer:
Cross-Site Request Forgery tricks a logged-in user's browser into sending an authenticated request to your site. Defenses: SameSite cookies, anti-CSRF tokens on state-changing forms, and never use cookies for cross-origin POST without proof of intent.
// Server sends a CSRF token in a meta tag
fetch("/transfer", {
method: "POST",
headers: { "X-CSRF-Token": document.querySelector("meta[name=csrf]").content }
});Answer:
Prototype pollution is when attacker input modifies Object.prototype (or another shared prototype), affecting every object in the program. Often happens with unsafe deep-merge of JSON. Defense: validate input keys, use Object.create(null), freeze prototypes.
// Vulnerable merge
function merge(target, src) {
for (const k in src) {
if (typeof src[k] === "object") merge(target[k] ??= {}, src[k]);
else target[k] = src[k];
}
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
({}).isAdmin; // true — polluted!Answer:
Detached DOM nodes still referenced by JS, forgotten timers, unremoved event listeners, growing global state, and closures holding large objects. Fix by cleaning up in component teardown and using WeakMap/WeakRef where appropriate.
// Leak — no way to remove the listener
function bindLeak(el) {
el.addEventListener("click", () => console.log(el.dataset));
}
// Fix — return a cleanup function
function bind(el) {
const handler = () => console.log(el.dataset);
el.addEventListener("click", handler);
return () => el.removeEventListener("click", handler);
}Answer:
Break long tasks into smaller chunks (yield to the event loop), move heavy work to Web Workers, use requestIdleCallback for background tasks, virtualize long lists, and avoid forced synchronous layout.
async function processAll(items) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 100 === 0) await new Promise(r => setTimeout(r, 0));
}
}Answer:
Layout thrashing happens when you alternate reads and writes to the DOM in a loop, forcing the browser to recalculate layout each iteration. Batch reads, then batch writes.
// Thrashing
for (const el of elements) {
el.style.width = el.offsetWidth + 10 + "px"; // read + write
}
// Batched
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, i) => el.style.width = widths[i] + 10 + "px");Answer:
?. short-circuits to undefined if the left side is null or undefined. ?? returns the right side only if the left is null or undefined (unlike ||, which also triggers on 0 and "").
const user = { profile: null };
user.profile?.name; // undefined (no error)
const count = 0;
count || 10; // 10 — wrong, 0 is valid
count ?? 10; // 0 — correctAnswer:
||=, &&=, and ??= combine logical operators with assignment. They only assign if the condition holds.
let a = null;
a ??= "default"; // a = "default"
let b = "";
b ||= "fallback"; // b = "fallback"
let c = { name: "X" };
c.name &&= c.name.toUpperCase(); // "X"Answer:
BigInt is for integers beyond Number.MAX_SAFE_INTEGER (2^53 - 1). Append n to literals. You cannot mix BigInt and Number in arithmetic.
const big = 9007199254740993n;
big + 1n; // 9007199254740994n
// big + 1; // TypeError — cannot mix
typeof big; // "bigint"Answer:
Top-level await lets you use await directly in an ES module's top-level code, without wrapping in an async function. Available in ESM only.
// data.mjs
const res = await fetch("/api/init");
export const config = await res.json();Answer:
structuredClone deep-copies an object, including nested arrays, Maps, Sets, Dates, and typed arrays. Replaces the old JSON.parse(JSON.stringify(x)) trick, which loses non-JSON data.
const original = { date: new Date(), set: new Set([1, 2]) };
const copy = structuredClone(original);
copy.date.getTime() === original.date.getTime(); // true
copy.set instanceof Set; // trueAnswer:
Underscores in numeric literals make them readable. They have no effect on the value.
const billion = 1_000_000_000;
const hex = 0xFF_FF_FF;
const bin = 0b1010_0001;Answer:
at(i) returns the element at index i, supporting negative indices to count from the end. Cleaner than arr[arr.length - 1].
const arr = [1, 2, 3, 4];
arr.at(0); // 1
arr.at(-1); // 4
arr.at(-2); // 3Answer:
Object.hasOwn(obj, key) is the safe modern replacement. The old obj.hasOwnProperty(key) breaks on objects created with Object.create(null) or when hasOwnProperty is overridden.
const o = Object.create(null);
o.foo = 1;
// o.hasOwnProperty("foo"); // TypeError — no method
Object.hasOwn(o, "foo"); // trueAnswer:
new Error(msg, { cause }) lets you wrap a low-level error inside a higher-level one without losing the original. Inspect with err.cause.
try {
await fetch("/api");
} catch (e) {
throw new Error("Failed to load profile", { cause: e });
}Answer:
WeakRef lets you hold a reference that does not prevent garbage collection. FinalizationRegistry runs a callback when an object is collected. Useful for caches and observability — but use sparingly.
const ref = new WeakRef(largeObject);
const reg = new FinalizationRegistry((tag) => console.log("GC'd:", tag));
reg.register(largeObject, "largeObject");Answer:
ES2024's Promise.withResolvers() returns a fresh Promise plus its resolve and reject functions, removing the need to capture them inside a constructor.
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve("done"), 100);
await promise; // "done"Answer:
When firing async requests as the user types, you must drop responses from old requests so the latest wins. Use a counter or AbortController.
let latest = 0;
async function search(q) {
const id = ++latest;
const res = await fetch(`/q?term=${q}`).then(r => r.json());
if (id !== latest) return; // stale, ignore
render(res);
}Answer:
[Symbol.toPrimitive] is a method JavaScript calls to convert your object to a primitive when used in arithmetic, string templates, or comparisons. Lets you control coercion.
class Money {
constructor(amount) { this.amount = amount; }
[Symbol.toPrimitive](hint) {
if (hint === "number") return this.amount;
if (hint === "string") return `$${this.amount}`;
return `${this.amount}`;
}
}
const m = new Money(50);
+m; // 50 (number hint)
`${m}`; // "$50" (string hint)Answer:
A tag is a function that receives the literal's static strings and dynamic values separately. Used for safe HTML, SQL escaping, i18n, and styled components.
function html(strings, ...values) {
return strings.reduce((out, s, i) => {
const v = values[i] ? String(values[i]).replace(/</g, "<") : "";
return out + s + v;
}, "");
}
const name = "<script>";
html`<p>Hello ${name}</p>`;
// "<p>Hello <script></p>"Answer:
globalThis is the universal way to reach the global object in any environment — window in browsers, global in Node, self in workers.
globalThis.appVersion = "1.0";
console.log(globalThis.appVersion);Answer:
new Date() objects compare by reference with ===, not by time. Use .getTime() or numeric coercion.
const a = new Date("2024-01-01");
const b = new Date("2024-01-01");
a === b; // false
a.getTime() === b.getTime(); // true
+a === +b; // trueAnswer:
JavaScript uses IEEE-754 double-precision floats. 0.1 and 0.2 cannot be represented exactly, so their sum is 0.30000000000000004. Compare with a tolerance for floating-point math, or use BigInt/decimal libraries for money.
0.1 + 0.2; // 0.30000000000000004
Math.abs(0.1 + 0.2 - 0.3) < 1e-9; // true — safe comparisonAnswer:
for...in iterates enumerable keys including inherited ones (avoid for arrays). for...of iterates iterable values (arrays, strings, Maps, Sets). forEach is an array method that cannot break or use await cleanly.
const arr = ["a", "b", "c"];
for (const key in arr) console.log(key); // "0", "1", "2"
for (const val of arr) console.log(val); // "a", "b", "c"
arr.forEach(v => console.log(v)); // "a", "b", "c"Answer:
Object.entries turns an object into [key, value] pairs. Object.fromEntries does the reverse. Together they enable easy object transformation.
const user = { name: "Iqbal", age: 25 };
const entries = Object.entries(user);
// [["name","Iqbal"], ["age",25]]
const upper = Object.fromEntries(
entries.map(([k, v]) => [k.toUpperCase(), v])
);
// { NAME: "Iqbal", AGE: 25 }Answer:
Falsy values: false, 0, -0, 0n, "", null, undefined, NaN. Everything else is truthy, including empty arrays and empty objects (which trips up many devs).
Boolean([]); // true — empty array is truthy
Boolean({}); // true — empty object is truthy
Boolean("0"); // true — non-empty string is truthy
Boolean(0); // false
Boolean(""); // falseAnswer:
All three return arrays. keys gives property names, values gives values, and entries gives [key, value] pairs. They only return own, enumerable properties.
const o = { a: 1, b: 2 };
Object.keys(o); // ["a", "b"]
Object.values(o); // [1, 2]
Object.entries(o); // [["a",1], ["b",2]]Q94. What is JSON.stringify and its hidden options?
Answer:
JSON.stringify(value, replacer, space) converts to JSON. The replacer can be a function or an array of allowed keys. The third argument adds indentation. Functions, undefined, and symbols are dropped.
const obj = { a: 1, b: 2, secret: "x" };
JSON.stringify(obj, ["a", "b"], 2);
// '{
// "a": 1,
// "b": 2
// }'Answer:
Logical operators stop evaluating once the result is determined. a && b only evaluates b if a is truthy. a || b only evaluates b if a is falsy. Used for guards and defaults.
user && user.name; // safe access
name || "Anonymous"; // default value
isLoggedIn && fetchData(); // conditional callAnswer:
A RegExp matches patterns in strings. Created with /pattern/flags or new RegExp(...). Common flags: g (global), i (case-insensitive), m (multiline). Use .test, .match, .replace, and String.matchAll.
const re = /(\w+)@(\w+\.\w+)/;
"hi iqbal@example.com".match(re);
// ["iqbal@example.com", "iqbal", "example.com", index: 3, ...]
"foo bar foo".replace(/foo/g, "baz"); // "baz bar baz"Answer:
instanceof checks whether an object's prototype chain includes a constructor's prototype. Useful for class checks but breaks across realms (iframes) and primitives.
class Cat {}
const c = new Cat();
c instanceof Cat; // true
c instanceof Object; // true
[] instanceof Array; // true
"hi" instanceof String; // false — primitive, not boxedAnswer:
flat(depth) flattens nested arrays. flatMap is map followed by flat(1) — useful for one-to-many transforms.
[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]]
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
["one two", "three four"].flatMap(s => s.split(" "));
// ["one", "two", "three", "four"]Answer:
slice(start, end) returns a copy of part of an array or string without modifying it. splice(start, count, ...add) mutates the array, removing and/or inserting items. split(sep) splits a string into an array.
const arr = [1, 2, 3, 4];
arr.slice(1, 3); // [2, 3] — arr unchanged
arr.splice(1, 2); // [2, 3] — arr is now [1, 4]
"a-b-c".split("-"); // ["a", "b", "c"]Answer:
Both shallow-copy properties. Object.assign(target, ...sources) mutates target and returns it. The spread operator creates a new object. Spread is more readable.
const a = { x: 1 };
const b = { y: 2 };
Object.assign({}, a, b); // { x: 1, y: 2 } — new object
({ ...a, ...b }); // { x: 1, y: 2 } — same result, cleaner
Object.assign(a, b); // mutates a → { x: 1, y: 2 }