Scope determines the accessibility and lifetime of variables in your code.
Variables declared outside any function or block are in the global scope. In browsers, they become properties of the window object.
var globalVar = "I'm global";
let globalLet = "Also global";
function test() {
console.log(globalVar); // Accessible
}Variables declared with var inside a function are function-scoped. They're accessible anywhere within that function but not outside.
function myFunction() {
var functionScoped = "Only inside function";
if (true) {
var stillFunctionScoped = "Still accessible";
}
console.log(stillFunctionScoped); // Works
}Introduced with ES6 (let and const), block scope confines variables to the block {} where they're declared.
if (true) {
let blockScoped = "Only in this block";
const alsoBlockScoped = "Me too";
var notBlockScoped = "I'm function/global scoped";
}
// blockScoped and alsoBlockScoped are NOT accessible here
// notBlockScoped IS accessibleInterview Insight: Understanding scope is fundamental to preventing variable collisions, memory leaks, and writing maintainable code. Senior developers should explain how scope affects closure creation and module patterns.
Scope chain is the mechanism JavaScript uses to resolve variable names. When a variable is referenced, JavaScript looks in the current scope, then moves up through parent scopes until it finds the variable or reaches the global scope.
const global = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(innerVar); // Found in inner scope
console.log(outerVar); // Found in outer scope (via scope chain)
console.log(global); // Found in global scope (via scope chain)
}
inner();
}Key Points:
- Each function creates a new scope that links to its parent scope
- The chain is determined at function definition time (lexical scoping), not at runtime
- If a variable isn't found in the entire chain, a
ReferenceErroris thrown
Performance Consideration: Deeply nested scope chains can impact performance. Variables in outer scopes require more lookups.
- String:
"hello" - Number:
42,3.14 - BigInt:
9007199254740991n - Boolean:
true,false - Undefined:
undefined - Null:
null - Symbol:
Symbol('description')
let a = 10;
let b = a; // Copies the VALUE
b = 20;
console.log(a); // 10 (unchanged)- Object:
{},[],function() {} - Array:
[1, 2, 3] - Function:
function() {} - Date, RegExp, etc.
let obj1 = { value: 10 };
let obj2 = obj1; // Copies the REFERENCE
obj2.value = 20;
console.log(obj1.value); // 20 (changed!)Memory Storage:
- Primitives are stored directly in the stack
- Objects are stored in the heap, with references stored in the stack
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (initialized with undefined) | Yes (but in TDZ) | Yes (but in TDZ) |
| Re-declaration | Allowed | Not allowed | Not allowed |
| Re-assignment | Allowed | Allowed | Not allowed* |
| Global object property | Yes | No | No |
*For const, the binding is immutable, but object properties can be modified.
// var - function scoped
function varTest() {
var x = 1;
if (true) {
var x = 2; // Same variable!
console.log(x); // 2
}
console.log(x); // 2
}
// let - block scoped
function letTest() {
let x = 1;
if (true) {
let x = 2; // Different variable
console.log(x); // 2
}
console.log(x); // 1
}
// const - must be initialized, binding is immutable
const obj = { value: 1 };
// obj = {}; // Error!
obj.value = 2; // AllowedBest Practice: Always use const by default. Use let only when you need reassignment. Avoid var in modern code.
The TDZ is the period between entering scope and the variable declaration being executed. During this time, accessing the variable throws a ReferenceError.
console.log(varVariable); // undefined (hoisted)
console.log(letVariable); // ReferenceError: Cannot access before initialization
var varVariable = "var";
let letVariable = "let";function example() {
// TDZ starts for 'x'
console.log(x); // ReferenceError
let x = 10; // TDZ ends
console.log(x); // 10
}Why TDZ exists: It helps catch programming errors. If you're using a variable before declaring it, it's likely a mistake.
Interview Insight: Explain that hoisting still occurs with let and const, but they remain uninitialized in the TDZ, unlike var which is initialized with undefined.
Hoisting is JavaScript's behavior of moving declarations to the top of their scope before code execution.
sayHello(); // Works!
function sayHello() {
console.log("Hello");
}console.log(x); // undefined (not ReferenceError)
var x = 5;
// Interpreted as:
// var x;
// console.log(x);
// x = 5;console.log(y); // ReferenceError
let y = 10;sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi");
};const obj = new MyClass(); // ReferenceError
class MyClass {}Prototypes are the mechanism by which JavaScript objects inherit features from one another. Every JavaScript object has an internal [[Prototype]] property that references another object.
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"Key Concepts:
- Every function has a
prototypeproperty (an object) - When you create an object with
new, the object's[[Prototype]]links to the constructor'sprototype - Access via
Object.getPrototypeOf(obj)orobj.__proto__(deprecated)
The prototype object is a regular object that serves as a template for other objects. It contains properties and methods that should be shared across all instances.
function Car(brand) {
this.brand = brand;
}
// Adding methods to prototype (shared across instances)
Car.prototype.start = function() {
console.log(`${this.brand} is starting`);
};
Car.prototype.wheels = 4; // Shared property
const tesla = new Car("Tesla");
const bmw = new Car("BMW");
tesla.start(); // "Tesla is starting"
console.log(tesla.wheels); // 4
console.log(bmw.wheels); // 4
// Modifying prototype affects all instances
Car.prototype.wheels = 6;
console.log(tesla.wheels); // 6
console.log(bmw.wheels); // 6Memory Efficiency: Methods defined on the prototype are shared, not duplicated for each instance.
Prototype chaining is how JavaScript implements inheritance. When accessing a property, JavaScript looks at the object, then its prototype, then the prototype's prototype, etc., until it finds the property or reaches null.
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} is barking`);
};
const dog = new Dog("Buddy", "Golden Retriever");
dog.bark(); // Own method
dog.eat(); // Inherited from Animal via prototype chain
// Lookup chain: dog -> Dog.prototype -> Animal.prototype -> Object.prototype -> nullES6 Class Syntax (syntactic sugar over prototypes):
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking`);
}
}A closure is a function that has access to variables from its outer (enclosing) lexical scope, even after the outer function has returned.
function createCounter() {
let count = 0; // Private variable
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
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 Use Cases:
- Data Privacy/Encapsulation: Creating private variables
- Event Handlers: Preserving state
- Callbacks: Maintaining context
- Partial Application: Creating specialized functions
Memory Consideration: Closures keep references to outer variables, preventing garbage collection. Be mindful of memory leaks with long-lived closures.
// Memory leak example
function addHandlers() {
const largeData = new Array(1000000).fill('data');
document.getElementById('btn').addEventListener('click', function() {
console.log('Clicked');
// largeData is kept in memory even if not used!
});
}JavaScript is always pass-by-value, but the "value" for objects is a reference.
function modifyPrimitive(x) {
x = 100;
}
let num = 50;
modifyPrimitive(num);
console.log(num); // 50 (unchanged)function modifyObject(obj) {
obj.value = 100; // Modifies the original object
}
function reassignObject(obj) {
obj = { value: 200 }; // Only changes local reference
}
let myObj = { value: 50 };
modifyObject(myObj);
console.log(myObj.value); // 100 (changed)
reassignObject(myObj);
console.log(myObj.value); // 100 (unchanged - reassignment doesn't affect original)Key Insight: You're passing a copy of the reference, not the reference itself. This is why reassigning the parameter doesn't affect the original variable.
Currying transforms a function with multiple arguments into a sequence of functions, each taking a single argument.
// Normal function
function add(a, b, c) {
return a + b + c;
}
// Curried version
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// ES6 arrow function syntax
const curriedAddArrow = a => b => c => a + b + c;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));
};
}
};
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6Benefits:
- Reusability: Create specialized functions
- Composition: Easier function composition
- Partial Application: Fix some arguments
Implement a function that can be called indefinitely and returns the sum when invoked without arguments.
function add(a) {
return function(b) {
if (b !== undefined) {
return add(a + b);
}
return a;
};
}
console.log(add(1)(2)(3)(4)()); // 10
console.log(add(5)(10)()); // 15Alternative with valueOf:
function add(a) {
let sum = a;
function inner(b) {
sum += b;
return inner;
}
inner.valueOf = function() {
return sum;
};
return inner;
}
console.log(+add(1)(2)(3)(4)); // 10With toString:
function add(a) {
let sum = a;
function inner(b) {
if (b === undefined) return sum;
sum += b;
return inner;
}
inner.toString = function() {
return sum;
};
return inner;
}
console.log(String(add(1)(2)(3))); // "6"Memoization is an optimization technique that caches the results of expensive function calls and returns the cached result when the same inputs occur again.
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('Fetching from cache');
return cache[key];
}
console.log('Calculating result');
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
// Example: Expensive Fibonacci
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(40)); // Calculating result
console.log(memoizedFib(40)); // Fetching from cache (instant)Advanced Memoization with WeakMap (for object arguments):
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = args[0];
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}Use Cases:
- Recursive calculations (Fibonacci, factorial)
- API calls with repeated parameters
- Complex computations in rendering
The rest parameter syntax allows representing an indefinite number of arguments as an array.
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15Key Points:
- Must be the last parameter
- Only one rest parameter per function
- Creates a real array (not array-like object like
arguments)
function logDetails(firstName, lastName, ...hobbies) {
console.log(firstName); // "John"
console.log(lastName); // "Doe"
console.log(hobbies); // ["reading", "coding", "gaming"]
}
logDetails("John", "Doe", "reading", "coding", "gaming");Rest vs Arguments:
function oldWay() {
const args = Array.from(arguments); // or [...arguments]
console.log(args);
}
function newWay(...args) {
console.log(args); // Already an array
}The spread operator expands an iterable (array, string, object) into individual elements.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Copying arrays
const copy = [...arr1]; // Shallow copy
// Passing array elements as arguments
Math.max(...arr1); // 3const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// Overriding properties
const updated = { ...obj1, b: 10 }; // { a: 1, b: 10 }
// Shallow copy
const objCopy = { ...obj1 };const str = "hello";
const chars = [...str]; // ["h", "e", "l", "l", "o"]Common Use Cases:
- Cloning arrays/objects (shallow)
- Merging arrays/objects
- Converting iterables to arrays
- Function arguments
const obj = {
name: "John",
age: 30
};function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person("John", 30);const proto = {
greet() {
console.log("Hello");
}
};
const obj = Object.create(proto);
obj.name = "John";class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person = new Person("John", 30);function createPerson(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
}
const person = createPerson("John", 30);const obj = new Object();
obj.name = "John";
obj.age = 30;const obj = Object.assign({}, { name: "John" }, { age: 30 });const singleton = (function() {
let instance;
function createInstance() {
return { name: "Singleton" };
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();Generator functions are special functions that can pause execution and resume later, yielding multiple values over time.
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 }Infinite Generator:
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const seq = infiniteSequence();
console.log(seq.next().value); // 0
console.log(seq.next().value); // 1
console.log(seq.next().value); // 2Practical Example - Custom Iterator:
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
for (const num of range(0, 10, 2)) {
console.log(num); // 0, 2, 4, 6, 8
}Delegating Generators:
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield* gen1(); // Delegate to gen1
yield 3;
}
const gen = gen2();
console.log([...gen]); // [1, 2, 3]Use Cases:
- Lazy evaluation
- Infinite sequences
- Custom iterators
- Async flow control (with libraries)
JavaScript is single-threaded, meaning it executes one task at a time in a single call stack. However, it can handle asynchronous operations through the event loop.
Why Single-Threaded?
- Originally designed for simple browser interactions
- Avoids complexity of multi-threading (race conditions, deadlocks)
- Simplifies the programming model
How Asynchronicity Works: JavaScript uses:
- Call Stack: Executes synchronous code
- Web APIs: Handle async operations (setTimeout, fetch, DOM events)
- Callback Queue: Stores callbacks from completed async operations
- Event Loop: Moves callbacks from queue to call stack when stack is empty
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
// Output:
// Start
// End
// Promise
// TimeoutConcurrency Model: JavaScript achieves concurrency through asynchronous callbacks, not parallel execution.
Callbacks are functions passed as arguments to other functions, executed after an operation completes.
- Asynchronous Operations: Handle operations that take time (I/O, network requests)
- Non-Blocking Code: Continue execution while waiting
- Event Handling: Respond to user actions
// Synchronous (blocking)
function syncOperation() {
// Blocks for 5 seconds
const end = Date.now() + 5000;
while (Date.now() < end) {}
return "Done";
}
// Asynchronous (non-blocking)
function asyncOperation(callback) {
setTimeout(() => {
callback("Done");
}, 5000);
}
asyncOperation((result) => {
console.log(result); // Executes after 5 seconds
});
console.log("This runs immediately");Real-World Example:
// Reading file (Node.js style)
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
// Event listener
button.addEventListener('click', (event) => {
console.log('Button clicked');
});The event loop is the mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded.
- Call Stack: Executes functions (LIFO)
- Web APIs: Browser/Node.js APIs handle async operations
- Callback Queue (Task Queue): FIFO queue for callbacks
- Microtask Queue: Higher priority queue for promises
- Event Loop: Continuously checks if call stack is empty, then pushes tasks
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2Execution Order:
- Execute all synchronous code (Call Stack)
- Execute all Microtasks (Promises)
- Execute one Macrotask (setTimeout, setInterval)
- Repeat
Visualization:
Call Stack
↓
Empty?
↓
Microtask Queue (Process ALL)
↓
Macrotask Queue (Process ONE)
↓
Render (if browser)
↓
Repeat
The callback queue holds callbacks from async operations like setTimeout, setInterval, and I/O operations.
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');
// Output:
// Start
// End
// Timeout 1
// Timeout 2Macrotasks include:
setTimeoutsetIntervalsetImmediate(Node.js)- I/O operations
- UI rendering
Key Point: Event loop processes one macrotask per iteration, then processes all microtasks before the next macrotask.
The microtask queue has higher priority than the callback queue. It holds:
- Promise callbacks (
.then,.catch,.finally) queueMicrotask()MutationObservercallbacks
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
.then(() => console.log('3'))
.then(() => console.log('4'));
console.log('5');
// Output: 1, 5, 3, 4, 2Microtask Priority: All microtasks are processed before the next macrotask, even if the microtask queue keeps getting new tasks.
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => {
console.log('Promise 1');
Promise.resolve().then(() => {
console.log('Promise 2');
});
});
// Output:
// Promise 1
// Promise 2
// TimeoutA Promise is an object representing the eventual completion or failure of an asynchronous operation.
- Pending: Initial state
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("Success!");
} else {
reject("Error!");
}
}, 1000);
});
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log("Cleanup"));Chaining:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log(data);
return data.id;
})
.then(id => fetch(`https://api.example.com/details/${id}`))
.then(response => response.json())
.catch(error => console.error('Error:', error));Promise Methods:
// Promise.all - All must resolve
Promise.all([promise1, promise2, promise3])
.then(results => console.log(results));
// Promise.race - First to settle wins
Promise.race([promise1, promise2])
.then(result => console.log(result));
// Promise.allSettled - Wait for all (ES2020)
Promise.allSettled([promise1, promise2])
.then(results => console.log(results));
// Promise.any - First to fulfill (ES2021)
Promise.any([promise1, promise2])
.then(result => console.log(result));Event propagation is the flow of events through the DOM tree. It has three phases:
- Capturing Phase: Event travels from root to target
- Target Phase: Event reaches target element
- Bubbling Phase: Event bubbles up from target to root
<div id="outer">
<div id="middle">
<button id="inner">Click Me</button>
</div>
</div>const outer = document.getElementById('outer');
const middle = document.getElementById('middle');
const inner = document.getElementById('inner');
outer.addEventListener('click', () => console.log('Outer'), true); // Capture
middle.addEventListener('click', () => console.log('Middle'), true); // Capture
inner.addEventListener('click', () => console.log('Inner')); // Bubble
outer.addEventListener('click', () => console.log('Outer Bubble'));
middle.addEventListener('click', () => console.log('Middle Bubble'));
// Clicking inner button outputs:
// Outer (capture)
// Middle (capture)
// Inner
// Middle Bubble
// Outer BubbleEvent bubbling is when an event propagates from the target element up through its ancestors to the root.
document.getElementById('child').addEventListener('click', function(e) {
console.log('Child clicked');
});
document.getElementById('parent').addEventListener('click', function(e) {
console.log('Parent clicked');
});
document.body.addEventListener('click', function(e) {
console.log('Body clicked');
});
// Clicking child outputs:
// Child clicked
// Parent clicked
// Body clickedDefault Behavior: Most events bubble by default (except focus, blur, load, scroll).
Event capturing is the opposite of bubbling - the event travels from the root down to the target element.
// Enable capturing by setting third parameter to true
element.addEventListener('click', handler, true);
document.body.addEventListener('click', () => {
console.log('Body - Capture');
}, true);
document.getElementById('parent').addEventListener('click', () => {
console.log('Parent - Capture');
}, true);
document.getElementById('child').addEventListener('click', () => {
console.log('Child - Capture');
}, true);
// Clicking child outputs:
// Body - Capture
// Parent - Capture
// Child - CapturestopPropagation() prevents the event from bubbling up or capturing down the DOM tree.
document.getElementById('child').addEventListener('click', function(e) {
console.log('Child clicked');
e.stopPropagation(); // Stops propagation
});
document.getElementById('parent').addEventListener('click', function(e) {
console.log('Parent clicked'); // Won't execute
});
// Only outputs: Child clickedstopImmediatePropagation(): Prevents other listeners on the same element from executing.
element.addEventListener('click', function(e) {
console.log('First listener');
e.stopImmediatePropagation();
});
element.addEventListener('click', function(e) {
console.log('Second listener'); // Won't execute
});preventDefault(): Prevents default browser behavior (different from stopPropagation).
link.addEventListener('click', function(e) {
e.preventDefault(); // Prevents navigation
console.log('Link clicked but not followed');
});Event delegation leverages event bubbling to handle events at a higher level rather than attaching listeners to each child element.
// Bad: Attaching listener to each item
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick);
});
// Good: Single listener on parent
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.matches('.item')) {
handleClick(e);
}
});Benefits:
- Better Performance: Fewer event listeners
- Dynamic Elements: Works with elements added after page load
- Memory Efficiency: Reduced memory footprint
Real Example:
document.getElementById('todo-list').addEventListener('click', function(e) {
if (e.target.matches('.delete-btn')) {
const todoItem = e.target.closest('.todo-item');
todoItem.remove();
}
if (e.target.matches('.edit-btn')) {
const todoItem = e.target.closest('.todo-item');
editTodo(todoItem);
}
});JavaScript automatically converts types when needed.
// String coercion
"5" + 2 // "52" (number to string)
"5" - 2 // 3 (string to number)
"5" * "2" // 10 (both to numbers)
// Boolean coercion
if ("hello") { } // true (non-empty string to true)
if (0) { } // false (0 to false)
// Equality coercion
"5" == 5 // true (coerces string to number)
null == undefined // true (special case)Developer explicitly converts types.
// To String
String(123) // "123"
(123).toString() // "123"
123 + "" // "123"
// To Number
Number("123") // 123
parseInt("123px") // 123
parseFloat("12.5") // 12.5
+"123" // 123 (unary plus)
// To Boolean
Boolean(1) // true
Boolean(0) // false
!!"hello" // true (double negation)Falsy Values: false, 0, "", null, undefined, NaN
Interview Tip: Always use === (strict equality) to avoid unexpected coercion.
Throttling ensures a function is called at most once in a specified time period, regardless of how many times the event is triggered.
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// Usage
const handleScroll = throttle(() => {
console.log('Scroll event');
}, 1000);
window.addEventListener('scroll', handleScroll);Advanced with leading/trailing options:
function throttle(func, delay, options = {}) {
let timeout;
let lastCall = 0;
const { leading = true, trailing = true } = options;
return function(...args) {
const now = Date.now();
if (!lastCall && !leading) {
lastCall = now;
}
const remaining = delay - (now - lastCall);
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
lastCall = now;
func.apply(this, args);
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
lastCall = leading ? Date.now() : 0;
timeout = null;
func.apply(this, args);
}, remaining);
}
};
}Use Cases:
- Scroll events
- Window resizing
- Mouse movement tracking
- API rate limiting
Debouncing delays function execution until after a specified time has passed since the last invocation.
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage
const handleSearch = debounce((query) => {
console.log('Searching for:', query);
// API call
}, 500);
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});Advanced with immediate execution:
function debounce(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);
}
};
}Throttle vs Debounce:
- Throttle: Executes at regular intervals (every N ms)
- Debounce: Executes after activity stops (waits N ms of silence)
Use Cases:
- Search input (type-ahead)
- Form validation
- Window resize handlers
- Auto-save functionality
JavaScript execution involves multiple phases:
Source Code → Lexical Analysis → Tokens → Syntax Analysis → AST
Lexical Analysis (Tokenization): Breaks code into tokens (keywords, identifiers, operators, etc.)
const x = 10;
// Tokens: ["const", "x", "=", "10", ";"]Syntax Analysis: Creates Abstract Syntax Tree (AST) from tokens, checks for syntax errors.
Modern JavaScript engines (V8, SpiderMonkey) use JIT compilation:
- Interpreter: Quickly executes code by interpreting bytecode
- Profiler (Monitor): Identifies "hot" code (frequently executed)
- Compiler: Optimizes hot code to machine code
V8 Engine Pipeline:
Source Code
↓
Parser → AST
↓
Ignition (Interpreter) → Bytecode
↓
TurboFan (Optimizing Compiler) → Optimized Machine Code
-
Memory Creation Phase (Creation/Compilation):
- Creates Execution Context
- Sets up Variable Environment
- Hoisting occurs (variables/functions moved to top)
thisbinding determined
-
Code Execution Phase:
- Line-by-line code execution
- Variable assignments
- Function invocations
console.log(x); // undefined (hoisted)
var x = 10;
sayHi(); // "Hello" (function hoisted)
function sayHi() {
console.log("Hello");
}Optimization Techniques:
- Inline Caching: Caches property access patterns
- Hidden Classes: Optimizes object property access
- Escape Analysis: Determines if objects can be stack-allocated
Deoptimization: When assumptions about code change, the engine deoptimizes back to bytecode.
If a microtask continuously adds new microtasks, it can block the event loop (starve macrotasks).
// Problem: Infinite microtask loop
function infiniteMicrotask() {
Promise.resolve().then(() => {
console.log('Microtask');
infiniteMicrotask(); // Adds another microtask
});
}
infiniteMicrotask();
setTimeout(() => {
console.log('This never runs'); // Starved
}, 0);Solutions:
function controlledExecution() {
Promise.resolve().then(() => {
console.log('Microtask');
setTimeout(controlledExecution, 0); // Allows other tasks
});
}let count = 0;
function limitedMicrotask() {
if (count >= 1000) return; // Exit condition
Promise.resolve().then(() => {
count++;
limitedMicrotask();
});
}let taskCount = 0;
const MAX_TASKS = 100;
function safeMicrotask() {
if (taskCount >= MAX_TASKS) {
setTimeout(() => {
taskCount = 0;
safeMicrotask();
}, 0);
return;
}
queueMicrotask(() => {
taskCount++;
// Do work
safeMicrotask();
});
}Browser Protection: Modern browsers have safeguards against infinite microtask loops, but it's still a problem to avoid.
In JavaScript, functions are first-class citizens, meaning they can be:
- Assigned to variables
- Passed as arguments
- Returned from other functions
- Stored in data structures
// 1. Assigned to variables
const greet = function() {
console.log("Hello");
};
// 2. Passed as arguments
function execute(fn) {
fn();
}
execute(greet);
// 3. Returned from functions
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10
// 4. Stored in data structures
const operations = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
console.log(operations.add(5, 3)); // 8Higher-Order Functions: Functions that take functions as arguments or return functions.
// Array methods are higher-order functions
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
[1, 2, 3].filter(x => x > 1); // [2, 3]
[1, 2, 3].reduce((sum, x) => sum + x, 0); // 6IIFE is a function that executes immediately after being defined.
// Basic IIFE
(function() {
console.log("I run immediately");
})();
// Arrow function IIFE
(() => {
console.log("Arrow IIFE");
})();
// With parameters
(function(name) {
console.log(`Hello, ${name}`);
})("John");Use Cases:
(function() {
var privateVar = "I'm private";
// privateVar is not accessible outside
})();const module = (function() {
let privateCount = 0;
return {
increment() {
privateCount++;
},
getCount() {
return privateCount;
}
};
})();
module.increment();
console.log(module.getCount()); // 1(async function() {
const data = await fetch('/api/data');
console.log(data);
})();Alternative Syntax:
!function() { }();
+function() { }();
-function() { }();
~function() { }();
void function() { }();These methods control the this context in function execution.
Invokes function immediately with specified this and individual arguments.
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: "John" };
greet.call(person, "Hello", "!"); // "Hello, John!"Similar to call, but takes arguments as an array.
greet.apply(person, ["Hi", "!!"]); // "Hi, John!!"
// Useful for Math functions
const numbers = [1, 5, 3, 9, 2];
Math.max.apply(null, numbers); // 9Returns a new function with this permanently bound (doesn't invoke immediately).
const boundGreet = greet.bind(person);
boundGreet(); // "John"Interview Tips:
- Regular functions:
thisdetermined by call-site - Arrow functions:
thisdetermined by lexical scope - Use arrow functions to preserve
thisin callbacks
Hoisted and can be called before definition.
greet(); // Works
function greet() {
console.log("Hello");
}Not hoisted, assigned to variable.
// greet(); // Error
const greet = function() {
console.log("Hello");
};
greet(); // Worksconst factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1);
};
// fact is only available inside the functionFunction without a name (often used as callbacks).
setTimeout(function() {
console.log("Anonymous");
}, 1000);
[1, 2, 3].map(function(x) {
return x * 2;
});Concise syntax, lexical this, no arguments object.
// Basic
const add = (a, b) => a + b;
// With block body
const multiply = (a, b) => {
return a * b;
};
// Single parameter (parentheses optional)
const square = x => x * x;
// No parameters
const sayHi = () => console.log("Hi");
// Returning object (wrap in parentheses)
const makeObj = (name) => ({ name });Key Differences:
| Feature | Regular Function | Arrow Function |
|---|---|---|
this binding |
Dynamic | Lexical |
arguments object |
Yes | No (use rest) |
| Constructor | Can use new |
Cannot |
prototype |
Yes | No |
| Hoisting | Declaration only | No |
Both async and defer are attributes for <script> tags that control script loading and execution.
<script src="script.js"></script>- HTML parsing pauses
- Script downloads
- Script executes
- HTML parsing resumes
<script src="script.js" defer></script>- Scripts download in parallel with HTML parsing
- Execute after HTML parsing completes
- Execute in order they appear
- Execute before
DOMContentLoaded
<script src="script.js" async></script>- Scripts download in parallel with HTML parsing
- Execute as soon as downloaded (pauses HTML parsing)
- Execute in unpredictable order
- May execute before or after
DOMContentLoaded
Comparison Table:
| Attribute | Download | Execution | Order | Use Case |
|---|---|---|---|---|
| None | Blocks HTML | Immediate | Sequential | Legacy |
| defer | Parallel | After HTML parsing | Sequential | Scripts that need DOM |
| async | Parallel | ASAP | Unpredictable | Independent scripts (analytics) |
Visual Timeline:
Normal: [HTML] → [Download & Execute Script] → [HTML]
Defer: [HTML + Download] → [HTML Done] → [Execute Scripts in order]
Async: [HTML + Download] → [Execute when ready] → [HTML]
Best Practices:
- Use
deferfor most scripts (maintains order) - Use
asyncfor independent scripts (analytics, ads) - Place scripts at end of
<body>if not using defer/async
Execution Context is the environment where JavaScript code is evaluated and executed.
-
Global Execution Context (GEC)
- Created when script first runs
- Only one per program
- Creates global object (
windowin browsers)
-
Function Execution Context (FEC)
- Created when function is invoked
- New context for each function call
-
Eval Execution Context
- Code executed inside
eval()
- Code executed inside
- Create Variable Environment
- Create Lexical Environment
- Determine
thisbinding
// During creation phase:
function example() {
console.log(a); // undefined (hoisted but not initialized)
console.log(b); // ReferenceError (in TDZ)
var a = 10;
let b = 20;
}Variable Environment:
{
a: undefined, // var is hoisted with undefined
example: <function> // function declaration hoisted
}Lexical Environment:
{
b: <uninitialized>, // let/const in TDZ
const c: <uninitialized>
}Line-by-line code execution with actual assignments.
function outer() {
var x = 10;
function inner() {
var y = 20;
console.log(x + y); // Access outer's variable
}
inner();
}
outer();Execution Context Stack:
1. Global EC created (at bottom)
2. outer() EC pushed
3. inner() EC pushed
4. inner() completes, popped
5. outer() completes, popped
6. Global EC remains
The call stack is a LIFO (Last-In-First-Out) data structure that tracks function execution.
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:
// [global]
// [global, first]
// [global, first, second]
// [global, first, second, third]
// [global, first, second]
// [global, first]
// [global]
// Output:
// First
// Second
// Third
// Second again
// First againOccurs when call stack exceeds maximum size (usually recursion without base case).
function recursiveFunction() {
recursiveFunction(); // No base case
}
recursiveFunction(); // RangeError: Maximum call stack size exceededMaximum Stack Size:
- Chrome: ~10,000-15,000 calls
- Firefox: ~50,000 calls
- Varies by browser and available memory
Stack Trace:
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error("Stack trace");
}
a();
// Error with stack trace:
// Error: Stack trace
// at c (...)
// at b (...)
// at a (...)Garbage Collection (GC) automatically frees memory by removing objects no longer referenced.
Modern JavaScript engines (V8) use this approach:
- Mark Phase: Traverse from roots (globals, call stack) and mark reachable objects
- Sweep Phase: Delete unmarked objects
let obj1 = { data: "Important" };
let obj2 = { data: "Also important" };
obj1 = null; // obj1 now eligible for GC (no references)
// obj2 still reachable, won't be collectedTracks number of references; collects when count reaches zero.
Problem: Circular References
function createCircular() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1; // Circular reference
return "Created";
}
createCircular();
// Mark-and-sweep handles this
// Reference counting doesn't (memory leak)1. Global Variables
function leak() {
globalVar = "I leak"; // Forgot 'let/const'
}2. Forgotten Timers
const intervalId = setInterval(() => {
// Holds references even when no longer needed
}, 1000);
// Always clear: clearInterval(intervalId);3. Closures
function createLeak() {
const largeData = new Array(1000000).fill("data");
return function() {
console.log("Closure keeps largeData in memory");
};
}4. Detached DOM Nodes
const button = document.getElementById('btn');
document.body.removeChild(button);
// If you still have a reference to 'button', it won't be GC'd5. Event Listeners Not Removed
const element = document.getElementById('el');
element.addEventListener('click', handleClick);
// Later: element.removeEventListener('click', handleClick);- Nullify large objects when done
- Clear timers and intervals
- Remove event listeners
- Use WeakMap/WeakSet for object references that shouldn't prevent GC
// WeakMap doesn't prevent GC
const weakMap = new WeakMap();
let obj = { data: "test" };
weakMap.set(obj, "value");
obj = null; // obj can be garbage collected even though it's in weakMapCompares type and value without coercion.
5 === 5 // true
5 === "5" // false (different types)
null === null // true
undefined === undefined // true
null === undefined // false
// Objects compared by reference
{} === {} // false (different references)
[] === [] // false
const obj = {};
obj === obj // true (same reference)Performs type coercion before comparison.
5 == "5" // true (string coerced to number)
0 == false // true
"" == false // true
null == undefined // true (special case)
// Unexpected results
"0" == false // true
[] == false // true
[] == ![] // true (!)More precise than ===:
// Same as ===
Object.is(5, 5) // true
// Differences:
+0 === -0 // true
Object.is(+0, -0) // false
NaN === NaN // false
Object.is(NaN, NaN) // trueType Coercion Rules (==):
null == undefined // true
number == string // Convert string to number
boolean == anything // Convert boolean to number
object == primitive // Call valueOf() then toString()Best Practice: Always use === to avoid unexpected coercion bugs.
Special Cases:
// Checking for null or undefined
if (value == null) {
// true if value is null OR undefined
}
// Equivalent to:
if (value === null || value === undefined) {
// ...
}Strict mode makes JavaScript more secure and catches common mistakes.
// Entire script
"use strict";
function test() {
// Strict mode applies here
}
// Single function
function strictFunction() {
"use strict";
// Only strict here
}
// ES6 modules are strict by default
export function myFunction() {
// Strict mode
}1. Prevents Accidental Globals
"use strict";
function test() {
variable = 10; // ReferenceError (forgot let/const)
}2. Throws on Assignment to Read-Only Properties
"use strict";
const obj = {};
Object.defineProperty(obj, "prop", { value: 10, writable: false });
obj.prop = 20; // TypeError3. Deleting Variables/Functions Not Allowed
"use strict";
let x = 10;
delete x; // SyntaxError
function test() {}
delete test; // SyntaxError4. Duplicate Parameter Names Not Allowed
"use strict";
function sum(a, a, b) { // SyntaxError
return a + a + b;
}5. Octal Literals Not Allowed
"use strict";
const num = 010; // SyntaxError (use 0o10 instead)6. this is undefined in Functions
"use strict";
function test() {
console.log(this); // undefined (not global object)
}
test();7. with Statement Not Allowed
"use strict";
with (obj) { // SyntaxError
// ...
}8. eval Creates Separate Scope
"use strict";
eval("var x = 10;");
console.log(x); // ReferenceError (x not in outer scope)Benefits:
- Catches silent errors
- Prevents unsafe actions
- Makes code optimization easier
- Prepares code for future JS versions
A Lexical Environment is a structure that holds identifier-variable mapping and a reference to the outer environment.
- Environment Record: Stores variables and function declarations
- Outer Environment Reference: Link to parent lexical environment
const global = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(innerVar); // Own environment
console.log(outerVar); // Outer environment
console.log(global); // Global environment
}
inner();
}
outer();Lexical Environment Chain:
Inner LE: { innerVar: "inner", outer: → Outer LE }
Outer LE: { outerVar: "outer", outer: → Global LE }
Global LE: { global: "global", outer: null }
- Check current Lexical Environment
- If not found, check outer environment
- Continue until variable is found or reach global scope
- If still not found,
ReferenceError
function makeCounter() {
let count = 0; // Stored in makeCounter's LE
return function() {
count++; // Accesses outer LE
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// Lexical environment of makeCounter is preservedKey Points:
- Created during function creation (not invocation)
- Determines scope chain
- Basis for closures
- Garbage collected when no longer referenced
- Scope & Closures: Understand lexical scoping and closure patterns
- Async Programming: Event loop, promises, async/await
- Prototypes: Inheritance mechanism in JavaScript
- this Binding: Context determination in different scenarios
- Event Loop: Microtasks vs macrotasks
- Implement polyfills (bind, map, reduce)
- Debounce/throttle implementations
- Closure-based problems (counter, cache)
- Promise chaining and error handling
- Async iteration patterns
- Minimize closure memory retention
- Use appropriate data structures (Map, Set, WeakMap)
- Understand garbage collection triggers
- Optimize event handlers with delegation
- Leverage async patterns for non-blocking code
- Prefer
const>let> nevervar - Always use
===over== - Enable strict mode
- Use arrow functions for lexical
this - Handle errors in promises/async functions
- Clean up event listeners and timers
Pro Tip: For 8+ years experience, interviewers expect:
- Deep understanding of "why" not just "how"
- Ability to explain trade-offs
- Knowledge of performance implications
- Experience with real-world debugging scenarios
- Understanding of browser/Node.js internalseet.bind(person, "Hey"); boundGreet("?"); // "Hey, John?"
// Useful for event handlers class Counter { constructor() { this.count = 0; this.increment = this.increment.bind(this); }
increment() { this.count++; } }
const counter = new Counter(); button.addEventListener('click', counter.increment);
**Polyfill for bind** (Interview Question):
```javascript
Function.prototype.myBind = function(context, ...args) {
const fn = this;
return function(...newArgs) {
return fn.apply(context, [...args, ...newArgs]);
};
};
Why Use These?
- Borrowing methods from other objects
- Explicit
thisbinding - Partial application
- Event handler context preservation
MapLimit processes an array of items with a maximum concurrency limit.
async function mapLimit(array, limit, asyncFunction) {
const results = [];
const executing = [];
for (const [index, item] of array.entries()) {
const promise = Promise.resolve().then(() => asyncFunction(item, index));
results.push(promise);
if (limit <= array.length) {
const executing = promise.then(() =>
executing.splice(executing.indexOf(executing), 1)
);
executing.push(executing);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
// Usage
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
mapLimit(urls, 2, async (url) => {
const response = await fetch(url);
return response.json();
});Better Implementation:
async function mapLimit(array, limit, asyncFn) {
const results = [];
const queue = array.map((item, index) => ({ item, index }));
async function worker() {
while (queue.length > 0) {
const { item, index } = queue.shift();
results[index] = await asyncFn(item, index);
}
}
const workers = Array(Math.min(limit, array.length))
.fill()
.map(() => worker());
await Promise.all(workers);
return results;
}Use Cases:
- Batch API requests with rate limiting
- Parallel file processing
- Database operations with connection pooling
async/await provides syntactic sugar for working with Promises, making asynchronous code look synchronous.
// Returns a Promise
async function fetchData() {
return "data"; // Automatically wrapped in Promise.resolve()
}
fetchData().then(data => console.log(data)); // "data"Pauses execution until Promise resolves (only works in async functions).
async function getData() {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}async function fetchWithErrorHandling() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error; // Re-throw or handle
}
}// Sequential (slow)
async function sequential() {
const result1 = await fetch('/api/1');
const result2 = await fetch('/api/2');
return [result1, result2];
}
// Parallel (fast)
async function parallel() {
const [result1, result2] = await Promise.all([
fetch('/api/1'),
fetch('/api/2')
]);
return [result1, result2];
}// In modules
const data = await fetch('/api/data');
console.log(data);Common Pitfalls:
// ❌ Wrong: Awaiting in loops (sequential)
async function processItems(items) {
for (const item of items) {
await processItem(item); // Slow
}
}
// ✅ Better: Parallel processing
async function processItems(items) {
await Promise.all(items.map(item => processItem(item)));
}then() and catch() are methods for handling Promise resolution and rejection.
Handles successful Promise resolution.
promise.then(onFulfilled, onRejected);
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.then(() => console.log('Done'));Handles Promise rejection.
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));Executes regardless of Promise outcome (ES2018).
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log('Cleanup'));Each then returns a new Promise.
Promise.resolve(1)
.then(x => x + 1) // 2
.then(x => x * 2) // 4
.then(x => {
throw new Error('Oops');
})
.catch(err => {
console.log('Caught:', err.message);
return 10; // Recovery
})
.then(x => console.log(x)); // 10Error Propagation:
fetch('/api/1')
.then(r => r.json())
.then(data => fetch(`/api/${data.id}`))
.then(r => r.json())
.catch(err => {
// Catches errors from any step above
console.error(err);
});Variable shadowing occurs when a variable in an inner scope has the same name as a variable in an outer scope.
let x = 10;
function outer() {
let x = 20; // Shadows outer x
function inner() {
let x = 30; // Shadows outer's x
console.log(x); // 30
}
inner();
console.log(x); // 20
}
outer();
console.log(x); // 10Cannot shadow let/const with var in the same scope.
function test() {
let a = 10;
{
var a = 20; // SyntaxError: Identifier 'a' has already been declared
}
}Legal Shadowing:
function test() {
var a = 10;
{
let a = 20; // Legal (block-scoped)
console.log(a); // 20
}
console.log(a); // 10
}static defines methods/properties that belong to the class itself, not to instances.
class MathUtils {
static PI = 3.14159;
static add(a, b) {
return a + b;
}
static #privateStaticMethod() {
return "Private";
}
}
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
const utils = new MathUtils();
console.log(utils.add); // undefined (not on instance)Static Initialization Block (ES2022):
class Database {
static connection;
static {
// Runs once when class is evaluated
this.connection = createConnection();
}
}Use Cases:
- Utility functions
- Factory methods
- Constants
- Singleton pattern
class User {
constructor(name) {
this.name = name;
}
static fromJSON(json) {
const data = JSON.parse(json);
return new User(data.name);
}
}
const user = User.fromJSON('{"name":"John"}');Variable declared but not assigned a value.
let x;
console.log(x); // undefined
console.log(typeof x); // "undefined"
function test() {}
console.log(test()); // undefined (no return)
const obj = {};
console.log(obj.prop); // undefined (property doesn't exist)Variable not declared at all.
console.log(y); // ReferenceError: y is not defined
console.log(typeof y); // "undefined" (special behavior with typeof)Intentional absence of value (assigned by programmer).
let x = null;
console.log(x); // null
console.log(typeof x); // "object" (JavaScript bug)
// Common use: Resetting values
let user = { name: "John" };
user = null; // Explicitly emptyComparison:
undefined == null // true (loose equality)
undefined === null // false (different types)
typeof undefined // "undefined"
typeof null // "object" (historical bug)Best Practice:
- Use
undefinedfor uninitialized variables (default) - Use
nullto explicitly represent "no value"
Higher-order functions either:
- Take functions as arguments
- Return functions
// 1. Function as argument
function repeat(times, fn) {
for (let i = 0; i < times; i++) {
fn(i);
}
}
repeat(3, (i) => console.log(i)); // 0, 1, 2
// 2. 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)); // 15Built-in HOFs:
// map
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
// filter
[1, 2, 3, 4].filter(x => x % 2 === 0); // [2, 4]
// reduce
[1, 2, 3].reduce((sum, x) => sum + x, 0); // 6
// forEach
[1, 2, 3].forEach(x => console.log(x));
// sort
[3, 1, 2].sort((a, b) => a - b); // [1, 2, 3]Custom HOF Example:
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// Calling add with [2, 3]
// Result: 5Callback hell (Pyramid of Doom) occurs with deeply nested callbacks, making code hard to read and maintain.
// Callback hell
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getYetMoreData(c, function(d) {
getFinalData(d, function(e) {
console.log(e);
});
});
});
});
});Solutions:
function handleFinalData(e) {
console.log(e);
}
function handleYetMoreData(d) {
getFinalData(d, handleFinalData);
}
function handleEvenMoreData(c) {
getYetMoreData(c, handleYetMoreData);
}
getData(handleEvenMoreData);getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => getYetMoreData(c))
.then(d => getFinalData(d))
.then(e => console.log(e))
.catch(err => console.error(err));async function fetchData() {
try {
const a = await getData();
const b = await getMoreData(a);
const c = await getEvenMoreData(b);
const d = await getYetMoreData(c);
const e = await getFinalData(d);
console.log(e);
} catch (err) {
console.error(err);
}
}this refers to the context in which a function is executed. Its value depends on how the function is called.
console.log(this); // Window (browser) or global (Node.js)const obj = {
name: "John",
greet() {
console.log(this.name); // "John"
}
};
obj.greet();function showThis() {
console.log(this);
}
showThis(); // Window (non-strict) or undefined (strict mode)function Person(name) {
this.name = name; // `this` refers to new instance
}
const person = new Person("John");
console.log(person.name); // "John"Arrow functions don't have their own this - they inherit from lexical scope.
const obj = {
name: "John",
regularFunc: function() {
console.log(this.name); // "John"
},
arrowFunc: () => {
console.log(this.name); // undefined (inherits from outer scope)
}
};button.addEventListener('click', function() {
console.log(this); // Button element
});
button.addEventListener('click', () => {
console.log(this); // Window (arrow function)
});function greet() {
console.log(this.name);
}
const person = { name: "John" };
greet.call(person); // "John"
greet.apply(person); // "John"
const boundGreet = gr