In JavaScript we have three ways to declare variables — var, let, and const
var is the old way — it is function scoped, gets hoisted with undefined value, and can be redeclared. I never use var because it causes unexpected bugs.
let is block scoped — only accessible within the block it is declared. It can be reassigned but cannot be redeclared. I use let when I need to change the value.
const is also block scoped — but once assigned, value cannot be reassigned. It must be initialized at the time of declaration. I use const by default for everything.
Example:
js// var — function scoped
var a = 10;
var a = 20; // ✅ can redeclare
console.log(a); // 20
// let — block scoped
let b = 10;
b = 20; // ✅ can reassign
// let b = 30; ❌ cannot redeclare
// const — block scoped
const c = 10;
// c = 20; ❌ cannot reassign
Hoisting is JavaScript's behavior of moving variable and function declarations to the top of their scope before execution. var is hoisted and initialized as undefined. let and const are hoisted but not initialized — called temporal dead zone.
Example:
console.log(a); // undefined — var hoisted
var a = 10;
console.log(b); // ❌ ReferenceError
let b = 10;
console.log(c); // ❌ ReferenceError
const c = 10;
Temporal dead zone is the time between when let or const variable is hoisted and when it is actually initialized. Accessing variable in this zone gives ReferenceError.
Closure is when inner function remembers variables of outer function even after outer function is done.
Example:
function outer() {
let count = 0;
return function() { count++; console.log(count); }
}
Arrow functions are a concise way to write functions using the => syntax. They don't have their own 'this' binding — they inherit it from the parent scope. They can't be used as constructors. Arrow functions are ideal for callbacks and shorter function bodies.
A callback function is a function passed as an argument to another function, which is then called inside that function. Example: setTimeout(() => console.log('Done'), 1000) — the arrow function is a callback. Callbacks are useful for async operations, but can lead to callback hell with too much nesting.
Callback hell is when multiple callbacks are nested inside each other — making code look like a pyramid. It becomes very hard to read, debug, and maintain. This is also called 'Pyramid of Doom.
Example:
getData(function(data1) {
getUser(data1, function(data2) {
getOrder(data2, function(data3) {
getPayment(data3, function(data4) {
console.log("Final result", data4)
// Pyramid of doom! 😵
})
})
})
})
Promise is an object that represents the eventual completion or failure of an asynchronous operation. It has 3 states — Pending, Fulfilled, and Rejected. It solves callback hell by chaining .then() instead of nesting.
Example:
Promise.resolve().then(() => {
console.log('promise1')
}).then(() => {
console.log('promise2')
}).catch((err) => {
console.log('error', err)
})
Async/await is modern syntax for handling promises. async function returns a promise. await pauses execution until promise resolves. Makes async code look synchronous and easier to read. Use try/catch for error handling. Example: async function getData() { try { const data = await fetch(url); } catch (e) { console.log(e); } }
Example:
async function fetchData() {
try {
console.log('fetch start')
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await response.json()
console.log('fetch end', data)
} catch (err) {
console.log('error', err)
}
}
fetchData()
Synchronous code runs line by line — each line waits for the previous to finish. If one operation takes time, everything blocks. Asynchronous code doesn't wait — it continues executing other code while waiting for a task to complete. Examples: setTimeout, API calls, file operations are async. I use callbacks, promises, or async/await to handle async code.
Event loop is how JavaScript handles async code. JavaScript has a call stack (sync code), event queue (async callbacks), and event loop (checks if stack is empty, moves callbacks to stack). Process: setTimeout callback → goes to queue → event loop waits for stack to be empty → moves callback to stack. This is why setTimeout(fn, 0) still runs after other code.
setTimeout is a Web API function that executes a callback function after a specified delay in milliseconds. It runs only once. It is non-blocking — meaning JavaScript doesn't wait for it, it continues executing next lines. Internally it goes to macrotask queue and executes only when call stack is empty.
setInterval is similar but it executes the callback repeatedly at every specified interval — until we manually stop it using clearInterval. For example in a live cricket score update — we can fetch API every 5 seconds using setInterval.
GET is used to fetch data without changing anything. POST is used to create new data, like submitting a form. PUT is used to update the complete record. PATCH is used to update only specific fields. DELETE is used to remove a record. For example, in a user profile feature — GET fetches profile, POST creates it, PATCH updates just the name, DELETE removes the account
We send the token in the request headers using the Authorization key with the Bearer prefix. Like Authorization: Bearer <token>. The backend then reads this header, verifies the token — usually a JWT — and decides whether to allow or reject the request. This is how protected routes work
Both fetch and axios are used to make API calls in JavaScript. Fetch is built into the browser so no installation needed, but it doesn't automatically parse JSON — we have to call .json() manually. Axios is a third-party library, so we install it via npm, but it automatically parses JSON, has better error handling — for example fetch doesn't throw error on 404 but axios does — and it also supports request cancellation and interceptors which fetch doesn't
I use try-catch with async-await. In the catch block I check error.response.status to identify the type of error — like 401 means unauthorized, 500 means server error. I also show a user-friendly error message instead of crashing the app. For fetch, I additionally check response.ok because fetch doesn't throw on 4xx errors.
Destructuring extracts values from objects or arrays into separate variables in a single statement. Array: const [a, b] = [1, 2]. Object: const { name, age } = { name: 'John', age: 30 }. Reduces code, makes it cleaner. Can use default values and rename: const { name: username = 'Guest' } = obj.
Both use ... syntax but different purposes. Spread unpacks array/object: [...arr] copies array, {...obj} copies object. Rest collects into array: function fn(...args) collects all arguments into args array. In destructuring: const [first, ...rest] = [1, 2, 3] gives rest = [2, 3].
map transforms each element: [1,2,3].map(x => x*2) = [2,4,6]. filter keeps elements matching condition: [1,2,3].filter(x => x > 1) = [2,3]. reduce accumulates into single value: [1,2,3].reduce((sum, x) => sum + x) = 6. All take callbacks, return new arrays (or value). Use map for transformation, filter for selection, reduce for aggregation.
Object.keys(obj) returns array of keys. Object.values(obj) returns array of values. Object.entries(obj) returns key-value pairs. Object.assign(target, source) copies properties. Object.freeze(obj) prevents modifications. Object.create(proto) creates new object with prototype. These methods are essential for object manipulation.
Shallow copy copies only the first level — nested objects/arrays still point to the original. const copy = {...obj} or Object.assign() are shallow. Deep copy copies everything including nested values. Use JSON.parse(JSON.stringify(obj)) for deep copy, or libraries like lodash. Deep copy prevents accidentally modifying original data.
Event bubbling: event starts from target element and bubbles up to parent elements. Event capturing: event starts from window and goes down to target element. By default, addEventListener uses bubbling. To use capturing: addEventListener('click', handler, true). stopPropagation() stops bubbling/capturing.
Debouncing: execute function only after delay of no events. Example: search input waits for user to stop typing before searching. Throttling: execute function at most once in a time interval. Example: scroll event fires max once per 100ms. Use libraries or create custom implementations. Both improve performance by reducing function calls.
Use try/catch/finally blocks to handle errors. try contains code that might error, catch handles the error, finally runs regardless. Example: try { riskyCode(); } catch(e) { console.log(e); } finally { cleanup(); }. Can also throw custom errors. Proper error handling prevents app crashes.
A Higher Order Function is a function that either takes one or more functions as arguments, returns a function, or does both. Higher-order functions help write cleaner, reusable, and more maintainable code.
Example:
function greet(name) {
return "Hello " + name;
}
function processUser(callback, name) {
return callback(name);
}
console.log(processUser(greet, "Vikas")); // Output: Hello Vikas
Common Higher-Order Functions:
- map() - takes callback and applies to every element
- filter() - takes callback to filter elements
- reduce() - takes callback to accumulate values
- setTimeout() - takes callback to execute later
- Array methods like forEach, find, some, every all take callbacks
A pure function always produces the same output for the same input and has no side effects — doesn't modify external variables or make API calls. Example: const add = (a, b) => a + b; is pure. Pure functions are predictable, testable, and safe to use. Non-pure functions modify state or depend on external factors.
Callback is a function passed to another function to be executed later. Used heavily with async operations like setTimeout, fetch, event listeners. Problem: callback hell — too much nesting makes code hard to read. Solution: use promises or async/await instead.
== vs ===
Null vs Undefined
Type coercion1 + "2"
IIFE
this keyword
OOP (Object-Oriented Programming) is a programming paradigm based on objects that contain data (properties) and behavior (methods).
JavaScript supports OOP through objects, constructor functions, classes, and prototypes. OOP helps create reusable, maintainable, and scalable applications.
The four main principles of OOP are:
1. 🔐 Encapsulation
Encapsulation means wrapping data and methods into a single unit (class) and restricting direct access to internal details.
Example:
class User {
#password;
constructor(password) {
this.#password = password;
}
checkPassword(password) {
return this.#password === password;
}
}
Benefits:
- Better security
- Data protection
- Easier maintenance
2. 🧬 Inheritance
Inheritance allows one class to inherit properties and methods from another class.
Example:
class Animal {
speak() {
console.log("Animal speaks");
}
}
class Dog extends Animal {
bark() {
console.log("Dog barks");
}
}
Benefits:
- Code reusability
- Reduced duplication
- Easier maintenance
3. 🔄 Polymorphism
Polymorphism means the same method behaves differently depending on the object using it.
Example:
class Animal {
makeSound() {
console.log("Some sound");
}
}
class Dog extends Animal {
makeSound() {
console.log("Bark");
}
}
class Cat extends Animal {
makeSound() {
console.log("Meow");
}
}
Benefits:
- Flexible code
- Easier extensibility
- Cleaner architecture
4. 🧩 Abstraction
Abstraction means hiding complex implementation details and exposing only necessary functionality.
Example:
class Car {
start() {
this.#startEngine();
console.log("Car started");
}
#startEngine() {
console.log("Engine started");
}
}
Users only call start() and don't need to know the internal implementation.
Benefits:
- Reduced complexity
- Better security
- Easier code usage
In modern JavaScript, OOP is implemented using ES6 classes, although JavaScript is prototype-based under the hood. Classes provide a cleaner syntax for creating objects and applying OOP principles.