What you’ll learn
- ●Variables & Scope
- ●Data Types
- Type Coercion (== vs ===)Free account
- FunctionsFree account
- HoistingFree account
- ClosuresFree account
- this & bindingFree account
- Prototypes & inheritanceFree account
- ClassesFree account
- Arrays & methodsFree account
- Objects & destructuringFree account
- Event loop & asyncFree account
- PromisesFree account
- async / awaitFree account
- Higher-order fns & curryingFree account
- Debounce & throttleFree account
- Modules (ESM vs CommonJS)Free account
- DOM & eventsFree account
- Error handlingFree account
- Interview recapFree account
- ●Project 1: Todo (vanilla JS)
- Project 2: Debounced searchFree account
- Project 3: Polyfills (build your own)Free account
Variables & Scope
Think of variables as labelled boxes where you store things. But there are three kinds of boxes with different rules: one you can empty and refill AND replace, one you can only refill, and one that is sealed forever once packed.
In JavaScript those three boxes are var, let, and const.
var— the old box. It is function-scoped (it leaks out of{ }blocks likeif/for), can be re-declared and reassigned, and is hoisted — created early and pre-filled withundefined.let— the modern refillable box. It is block-scoped (lives only inside the nearest{ }), can be reassigned but not re-declared in the same scope.const— the sealed box. Block-scoped, and its binding cannot be reassigned. You must give it a value at declaration.
Scope = where a variable is visible. Global scope = everywhere. Function scope = only inside that function. Block scope = only inside the { } (works for let/const, NOT var).
🌍 Real-world example: A
forloop withlet igives each iteration its OWNi, so 3 click-handlers remember 0, 1, 2. Withvar iall three share ONEiand log the final value (3) — the classic interview bug.
💡 Hoisting = JavaScript moves declarations to the top of their scope before running the code.
💡 Temporal Dead Zone (TDZ) = the gap from the start of the block until a
let/constis declared.let/constARE hoisted but stay uninitialised in this zone — touching them throws aReferenceError. (vargivesundefinedinstead, no error.)
💡 The
constgotcha =constlocks the BINDING, not the value. Aconstobject or array is still mutable — you can change its properties or push to it; you just can't point the name at a whole new object.
Rules cheat-sheet: var → re-declare ✔ reassign ✔. let → re-declare ✘ reassign ✔. const → re-declare ✘ reassign ✘ (but object contents mutable ✔).
Standard definition: In JavaScript, var is function-scoped and hoisted as undefined, while let and const are block-scoped and hoisted into the Temporal Dead Zone; const additionally prevents reassignment of the binding but not mutation of the referenced object.
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1 (var leaked out)
// console.log(b); // ReferenceError: b is not defined
const user = { name: 'Aisha' };
user.name = 'Bilal'; // allowed: object is mutable
console.log(user.name);
// user = {}; // TypeError: Assignment to constant variable.Data Types
Think of a printed page in a book versus a library card. If I photocopy a page and hand it to you, you scribble on your copy and my page stays clean — two separate things. But if I hand you a library card that points to shelf B-42, and you go move that book, then when I walk to B-42 the book is moved for me too — we shared one thing, not two.
In JavaScript, values come in two families. Primitives behave like the photocopy: copy-by-value. Objects behave like the library card: copy-by-reference (you copy the address, not the book).
There are 7 primitive types: string, number, boolean, null, undefined, symbol, bigint. Everything else — arrays, functions, dates, plain {} — is an object.
typeof tells you the type: typeof "hi" → "string", typeof 42 → "number", typeof true → "boolean", typeof undefined → "undefined", typeof 10n → "bigint", typeof Symbol() → "symbol". But watch the famous bug: typeof null === "object" — this is a historic JS mistake that can never be fixed for backward compatibility. And typeof function(){} → "function" (a special sub-case of object).
🌍 Real-world example: You copy a user object to "back it up" before editing (
const backup = user), changeuser.name, and are shocked thatbackup.namechanged too. They were never two objects — both variables held the same address. That single misunderstanding causes a huge share of real-world bugs.
💡 primitive = a simple, single, immutable value (a plain fact like
5or"cat"). 💡 reference = an address pointing to an object in memory, not the object itself.
null vs undefined: undefined = JS's default "nothing here yet" — a variable declared but not assigned, or a missing property. null = you deliberately set "intentionally empty". Rule of thumb: undefined is the engine's absence, null is your absence.
NaN = "Not a Number", the number you get from broken math like 0/0 or Number("abc"). Gotcha: typeof NaN → "number" (it lives in the number type), and NaN === NaN → false (it is not equal to anything, even itself). Use Number.isNaN(x) to test for it.
Standard definition: JavaScript has 7 primitive types (string, number, boolean, null, undefined, symbol, bigint) that are copied by value and are immutable, and objects (including arrays and functions) that are copied by reference; typeof identifies a value's type, with the historic quirk that typeof null returns "object".
let a = 10;
let b = a; // copy-by-value
b++;
console.log(a, b); // 10 11 — independent
let user = { name: "Aman" };
let backup = user; // copy-by-REFERENCE (same address)
backup.name = "Riya";
console.log(user.name); // "Riya" — shared object mutated
console.log(typeof null, NaN === NaN); // object falseProject 1: Todo (vanilla JS)
What we're building: A Todo app in pure vanilla JavaScript — no React, no framework, no libraries. Just an input, an Add button, and a list where you can add items, tick them done, and delete them. It's small, but it drills the single most important pattern in all of UI programming: keep your data in one place (state), and draw the screen FROM that data. Master this and React later will feel obvious — because React is just this pattern automated.
Concepts it teaches: state as a plain array, the render()-from-state pattern, addEventListener, reading input values, event delegation (one listener for the whole list), immutable-ish updates (map/filter), and safe id generation.
Step 0 — The mental model (read this first, 60 seconds)
Here's the trap almost every beginner falls into: they add a todo by manually building an <li> and appending it, then to delete they manually find that <li> and remove it, then to toggle they manually flip a class on that <li>. Three different places all poking the DOM by hand. It works for five minutes, then the DOM and your data drift apart and everything breaks.
We do it the disciplined way instead — one direction only:
State is the single source of truth. The screen is just a picture OF the state.
We keep an array called todos. That array IS the truth. There is one function, render(), whose only job is: look at the todos array and rebuild the list on screen to match it. We never edit the DOM directly to add/remove/toggle. Instead we do two clean steps every single time:
- Change the data (push / map / filter the
todosarray). - Call
render()— it wipes the list and redraws it fresh from the array.
💡 State = the data your UI is built from. Here it's the
todosarray.
💡 render() = a function that reads state and produces the DOM to match it. The DOM is an output of state, never an independent thing you edit.
Every feature below is just "change the array, then render." Keep that picture in your head. Now let's build.
Step 1 — The HTML skeleton
First the bare page. Three things: a text box, an Add button, and an empty <ul> for the list.
<input id="todo-input" type="text" placeholder="What needs doing?" />
<button id="add-btn">Add</button>
<ul id="list"></ul>
<script src="app.js"></script>
What's happening: The <ul id="list"> starts empty — we do NOT hand-write any <li>s in the HTML. Every <li> you'll ever see gets created by JavaScript from the todos array. The three ids (todo-input, add-btn, list) are the hooks our JS will grab onto. That's the whole HTML — it never changes again. All the life is in app.js.
💡 We load the script at the bottom (after the elements) so the elements already exist in the DOM when the script runs and
document.getElementByIdcan find them.
Step 2 — State + a render() that draws from it
Now the heart of the app. Two things: the state array, and the function that turns it into DOM.
const input = document.getElementById('todo-input');
const addBtn = document.getElementById('add-btn');
const list = document.getElementById('list');
// STATE — the single source of truth
let todos = [];
// render() — read state, rebuild the <ul> to match it
function render() {
list.innerHTML = ''; // 1. wipe whatever is there
for (const todo of todos) { // 2. one <li> per item in state
const li = document.createElement('li');
li.dataset.id = todo.id; // stamp the id onto the element
li.className = todo.done ? 'done' : '';
li.textContent = todo.text;
const del = document.createElement('button');
del.textContent = 'x';
del.className = 'delete';
li.appendChild(del);
list.appendChild(li); // 3. put it in the page
}
}
render(); // draw once on load (empty list for now)
What's happening — trace it:
- We grab our three elements once, up top, and store them in variables so we don't re-query every time.
todos = []— state starts empty.render()does three moves: (a)list.innerHTML = ''blanks the whole list, (b) loops overtodosand builds one<li>per item, (c) appends each<li>to the<ul>.
The critical habit: render() rebuilds the list from scratch every time — it never tries to "just add the new one" or "just remove one." It throws the old list away and redraws the current truth. That's what keeps DOM and state perfectly in sync — they can't drift, because the DOM is regenerated from the array on every render.
💡
li.dataset.id = todo.idwrites adata-idattribute onto the<li>. We'll read it back later (in Step 4) to know which todo a clicked button belongs to. This is how we connect a DOM element back to its item in the array.
💡
textContent(notinnerHTML) puts the user's text in as plain text — so if someone types<script>it shows as literal characters, not runnable HTML. Small habit, big security win.
Step 3 — Add a todo (change the array, then render)
Now make the Add button work. This is the full loop for the first time.
let nextId = 1; // simple incrementing id counter
function addTodo() {
const text = input.value.trim();
if (text === '') return; // ignore empty input
todos.push({ id: nextId++, text: text, done: false }); // 1. change state
input.value = ''; // clear the box
render(); // 2. redraw from state
}
addBtn.addEventListener('click', addTodo);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addTodo(); // Enter also adds
});
What's happening — trace the whole trigger, click by click:
- You type "Buy milk" and click Add (or press Enter).
addTodo()runs.input.value.trim()reads what you typed and trims stray spaces. If it's empty, we bail early so you can't add blank rows.todos.push({ id: nextId++, text, done: false })— we add a new object to the state array. Each todo is{ id, text, done }: a unique id, the text, and adoneflag startingfalse.- We clear the input box so it's ready for the next entry.
render()runs — wipes the<ul>, loops the now-1-item array, builds one<li>, appends it. The new item appears on screen.
That's the pattern in full: push to array → render() rebuilds the <ul> → DOM updates. We did NOT create an <li> here and stick it in the list ourselves. We only touched the data; render() did the drawing.
💡 Why
id: nextId++and notMath.random()? Ids must be unique — we use them to find the right todo when you click delete.Math.random()can (rarely) produce the same number twice → two todos share an id → deleting one deletes the wrong item, a nasty intermittent bug. A counter (1, 2, 3, …) orcrypto.randomUUID()(a guaranteed-unique string) never collides. Use one of those, never random.
Step 4 — Event delegation (the pro move)
Here's the interesting problem. Each <li> has a delete button, and clicking the text should toggle done. But the <li>s don't exist yet when the page loads — they're created later, and re-created on every render(). So we can't attach listeners to them up front.
The naive fix is to attach a listener to each button inside render(), every time we redraw. That works but it's wasteful and error-prone: you re-wire dozens of listeners on every keystroke-worth of change. The clean solution is event delegation.
// ONE listener on the <ul> — handles clicks for ALL items, present and future
list.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return; // clicked empty space? ignore
const id = Number(li.dataset.id); // which todo? read the stamped id
if (e.target.classList.contains('delete')) {
deleteTodo(id); // clicked the x → delete
} else {
toggleTodo(id); // clicked the row → toggle done
}
});
What's happening — why this is the whole trick:
Clicks bubble: a click on the delete button bubbles up through its <li> up to the <ul>. So instead of a listener on every button, we put one listener on the parent <ul> and let every child's click float up to it.
e.targetis the exact element you clicked (the button, or the<li>text).e.target.closest('li')walks up from whatever you clicked to find the enclosing<li>— that's the row.li.dataset.idreads back thedata-idwe stamped inrender(), telling us which todo in the array this row is.- Then we branch: was the click on something with class
delete? Delete it. Otherwise, toggle it.
💡 Event delegation = attach ONE listener to a parent, and use
event.targetto figure out which child was clicked. Because the parent (<ul>) is permanent, it automatically covers items added later — no re-wiring on every render. This is the reason delegation beats per-item listeners.
💡
Number(li.dataset.id)— dataset values are always strings ("3"), but our array ids are numbers (3). We convert so the===comparison inside find/filter matches. Forgetting this is a classic "why won't it delete" bug.
Step 5 — Toggle and delete (map / filter, then render)
Now the two handlers the delegation calls. Both follow the exact same discipline: produce a new array, reassign todos, then render().
function toggleTodo(id) {
todos = todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t // flip done on the matching one
);
render();
}
function deleteTodo(id) {
todos = todos.filter(t => t.id !== id); // keep everyone EXCEPT this id
render();
}
What's happening:
- toggle:
mapwalks every todo. For the one whoseidmatches, we return a new object{ ...t, done: !t.done }— a copy withdoneflipped. Everyone else is returned unchanged.maphands back a brand-new array, we reassigntodos, thenrender()redraws — the matched row now getsclass="done"(Step 2 checkstodo.done), so CSS can strike it through. - delete:
filterkeeps every todo whose id is not the one clicked — dropping exactly the clicked item. New array, reassign,render(). The row vanishes because it's no longer in the arrayrender()draws from.
Notice both use map/filter, which return new arrays rather than mutating in place. We could splice instead, but building a fresh array is the same mindset React will later demand ("don't mutate state") — good habit to start now.
💡
{ ...t, done: !t.done }— the spread...tcopies all oft's fields, thendone: !t.doneoverrides just that one. Result: same todo,doneflipped, as a new object.
🔎 The full flow (recap the wiring)
Here's every trigger, end to end:
- Page loads → variables grabbed →
todos = []→render()draws an empty list. - Type "Buy milk" + click Add →
addTodo()→todos.push({id, text, done:false})(state changes) →render()rebuilds the<ul>→ the item appears. - Click the row text → click bubbles to the
<ul>'s ONE listener →e.target.closest('li')finds the row → readid→ not a delete button →toggleTodo(id)→mapflipsdone→render()→ row shows as done (strikethrough). - Click the x → bubbles to the same listener →
e.targethas classdelete→deleteTodo(id)→filterdrops it →render()→ row disappears.
Every single feature is the same two beats: change the todos array → call render(). You never surgically edited the DOM. The array is the truth; the screen is its reflection. That is exactly the discipline that makes big apps (and React) manageable.
✅ What you just learned
- State as a plain array —
todosis the single source of truth; every todo is{ id, text, done }. - The render()-from-state pattern — one function wipes and rebuilds the DOM from state; you never edit the DOM piecemeal.
- The universal loop — change the data → call render(). Every feature (add, toggle, delete) is just this.
- Event delegation — ONE listener on the parent
<ul>, useevent.target(+closest) to find which child was clicked; automatically covers items added later, so no re-wiring on every render. - map / filter for updates —
filterto delete,map(with spread) to toggle — return new arrays instead of mutating. - Safe ids — an incrementing counter or
crypto.randomUUID(), neverMath.random()(collisions break delete/toggle). datasetto link DOM ↔ data — stampdata-idon each<li>, read it back on click to know which array item you're acting on (rememberNumber(...)— dataset is always strings).
// --- HTML ---
// <input id="todo-input" type="text" placeholder="What needs doing?" />
// <button id="add-btn">Add</button>
// <ul id="list"></ul>
// --- app.js ---
const input = document.getElementById('todo-input');
const addBtn = document.getElementById('add-btn');
const list = document.getElementById('list');
let todos = []; // STATE — single source of truth
let nextId = 1; // safe, collision-free ids (not Math.random!)
function render() {
list.innerHTML = ''; // wipe
for (const todo of todos) { // rebuild from state
const li = document.createElement('li');
li.dataset.id = todo.id;
li.className = todo.done ? 'done' : '';
li.textContent = todo.text;
const del = document.createElement('button');
del.textContent = 'x';
del.className = 'delete';
li.appendChild(del);
list.appendChild(li);
}
}
function addTodo() {
const text = input.value.trim();
if (text === '') return;
todos.push({ id: nextId++, text, done: false }); // change state
input.value = '';
render(); // redraw
}
function toggleTodo(id) {
todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
render();
}
function deleteTodo(id) {
todos = todos.filter(t => t.id !== id);
render();
}
addBtn.addEventListener('click', addTodo);
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') addTodo(); });
// EVENT DELEGATION — one listener for the whole list
list.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return;
const id = Number(li.dataset.id);
if (e.target.classList.contains('delete')) deleteTodo(id);
else toggleTodo(id);
});
render();JavaScriptinterview questions & answers
10 sample questions below — 246+ in the full bank inside.
What does the await keyword do?
await pauses the async function until the Promise it is waiting on settles, then gives you the resolved value (or throws the rejection). It only pauses that one async function, not the whole program — other code keeps running. await can only be used inside an async function (or at top level in modules).
In simple terms: It's like pausing a video to grab water — your video waits, but the world outside keeps moving. Example: const user = await fetchUser(); — the next line won't run until fetchUser's Promise resolves, but the browser stays responsive meanwhile.
What does an async function return?
An async function always returns a Promise. If you return a plain value, JavaScript wraps it in a resolved Promise; if you throw, it returns a rejected Promise. So the caller must use await or .then() to get the actual value out.
In simple terms: Think of an async function like a courier who never hands you the parcel directly — they always hand you a tracking slip (a Promise), and you 'await' the parcel to actually arrive. Example: async function f() { return 5; } — calling f() gives you a Promise, not 5. You do const v = await f(); to get 5.
What is the difference between find and filter?
find returns the first single element that matches the callback (or undefined if none match), while filter returns a new array of all matching elements. Use find when you want one item, filter when you want many.
In simple terms: In a classroom, find is calling out 'first kid wearing glasses, stand up' — you get one student and stop looking. filter is 'everyone wearing glasses, stand up' — you get a whole group. Example: users.find(u => u.id === 5) returns that one user object, while users.filter(u => u.active) returns an array of all active users.
What is throttling in JavaScript?
Throttling limits a function to run at most once per fixed time window, no matter how often the event fires. If events keep coming, extra calls in that window are ignored until the cooldown ends. It suits high-frequency events like scroll or resize where you want steady, regular updates.
In simple terms: Think of a machine gun with a fire-rate limiter — hold the trigger, but it can only shoot once every second. Example: window.addEventListener('scroll', throttle(onScroll, 200)) runs onScroll at most 5 times per second even if scroll fires hundreds of times.
What does the filter method do?
filter returns a new array containing only the elements for which the callback returns a truthy value. The original array is untouched, and if nothing matches you get an empty array.
In simple terms: Imagine a fruit basket and a sieve that only lets ripe fruit through. filter is that sieve — it keeps items that pass the test and drops the rest, giving you a new smaller basket. Example: [1,2,3,4].filter(n => n % 2 === 0) returns [2,4], keeping only the even numbers.
What do some and every return, and how do they differ?
Both return a boolean. some returns true if at least one element passes the callback test; every returns true only if all elements pass. Both short-circuit: some stops at the first true, every stops at the first false.
In simple terms: Think of a fruit basket check. some is 'is there at least one rotten apple?' — one is enough to say yes. every is 'are ALL apples fresh?' — one rotten one makes it no. Example: [2,4,6].every(n => n % 2 === 0) is true (all even), and [1,2,3].some(n => n > 2) is true (3 qualifies).
How do you check if an array contains a value?
Use includes, which returns a boolean true/false. Older code uses indexOf, which returns the index or -1 if not found. includes is cleaner for a yes/no check and also correctly finds NaN, which indexOf cannot.
In simple terms: It's like asking 'is Rahul in this room?' includes just says yes or no, while indexOf tells you which seat number (or -1 meaning not here). Example: [1,2,3].includes(2) returns true, while [1,2,3].indexOf(2) returns 1. For NaN, [NaN].includes(NaN) is true but [NaN].indexOf(NaN) is -1.
In modern JavaScript, can you write catch without the error parameter?
Yes. Since ES2019 you can write catch { ... } without the (e) binding when you don't need the error object — this is called optional catch binding. It's handy when you only care that something failed, not why.
In simple terms: It's like a smoke alarm that just beeps 'something's wrong' without telling you which room — sometimes that's all you need. Example: try { return JSON.parse(str) } catch { return null } — you don't inspect the error, you just fall back to null.
What is the difference between map and forEach?
map returns a brand-new array where each element is the result of the callback, so it's used to transform data. forEach just runs the callback for each element and returns undefined, so it's used for side effects like logging. Neither mutates the original array.
In simple terms: Think of a row of students. forEach is a teacher walking down the row and marking attendance — she does something for each but hands you nothing back. map is a photocopier making a new sheet where every name is changed to uppercase — you get a whole new list. Example: [1,2,3].map(n => n*2) gives [2,4,6], but [1,2,3].forEach(n => n*2) gives undefined.
How is async/await different from .then() chains?
They do the same thing — async/await is syntax sugar over Promises. .then() chains callbacks, while async/await lets you write asynchronous code that reads top-to-bottom like synchronous code. Errors are handled with try/catch instead of .catch(), which is usually cleaner and easier to debug.
In simple terms: Same destination, different route: .then() is like leaving sticky notes for 'what to do when this finishes', while async/await is like writing a normal to-do list in order. Example: fetchUser().then(u => show(u)) becomes const u = await fetchUser(); show(u);
236+ more JavaScript questions inside
Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.
Unlock all questions — freeReady to practise JavaScript?
Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.