JavaScript Fundamentals — Interview Notes
Use this as a fast interview review sheet.
1. Closures
Definition
A closure happens when a function remembers variables from the scope where it was created, even after that outer function has finished executing.
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
increment() keeps access to count.
Common use cases
Private state
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
Classic closure interview bug
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
Output:
3
3
3
Fix:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
Output:
0
1
2
Interview answer
A closure is a function bundled with references to its lexical environment. It allows the function to access variables from its creation scope even when executed somewhere else later.
2. JavaScript Event Loop
JavaScript executes synchronous code on a single call stack.
Call Stack
|
v
Browser / Node APIs
|
+----------------+
| |
v v
Microtask Queue Task Queue
Promise setTimeout
queueMicrotask events
MutationObserver setInterval
| |
+-------+--------+
|
v
Event Loop
Important ordering:
1. Run synchronous code
2. Drain ALL microtasks
3. Run next task/macrotask
4. Drain microtasks again
5. Repeat
Interview example
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('C');
});
console.log('D');
Output:
A
D
C
B
More difficult example
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
2
3. Promise Chaining
A .then() returns a new Promise.
fetchUser()
.then((user) => {
return fetchOrders(user.id);
})
.then((orders) => {
return calculateTotal(orders);
})
.then((total) => {
console.log(total);
})
.catch((error) => {
console.error(error);
});
Returning a regular value
Promise.resolve(10)
.then((value) => {
return value * 2;
})
.then((value) => {
console.log(value);
});
Output:
20
Returning another Promise
Promise.resolve(10)
.then((value) => {
return Promise.resolve(value * 2);
})
.then((value) => {
console.log(value);
});
Common interview mistake
Bad:
fetchUser()
.then((user) => {
fetchOrders(user.id);
})
.then((orders) => {
console.log(orders);
});
Correct:
fetchUser()
.then((user) => {
return fetchOrders(user.id);
})
.then((orders) => {
console.log(orders);
});
Or:
fetchUser()
.then((user) => fetchOrders(user.id))
.then((orders) => console.log(orders));
Error propagation
doStep1()
.then(doStep2)
.then(doStep3)
.catch((error) => {
console.error(error);
});
4. Scope and Hoisting
JavaScript uses lexical scope.
Global scope
const appName = 'ChatApp';
Function scope
function test() {
var x = 10;
}
console.log(x); // ReferenceError
Block scope
if (true) {
let x = 10;
const y = 20;
}
console.log(x); // ReferenceError
console.log(y); // ReferenceError
Hoisting with var
console.log(x);
var x = 10;
Output:
undefined
Conceptually:
var x;
console.log(x);
x = 10;
let and const
console.log(x);
let x = 10;
Throws a ReferenceError because x is in the Temporal Dead Zone until initialization.
Function declaration
sayHello();
function sayHello() {
console.log('hello');
}
Works because function declarations are hoisted.
Function expression
sayHello();
const sayHello = function () {
console.log('hello');
};
Throws because sayHello is in the TDZ.
Quick comparison
var
- function scoped
- hoisted
- initialized undefined
let
- block scoped
- hoisted
- TDZ
const
- block scoped
- hoisted
- TDZ
- cannot reassign binding
function declaration
- fully hoisted
5. Pure Functions vs Side Effects
A pure function:
Same input -> same output
and does not modify external state.
function add(a, b) {
return a + b;
}
Impure function
let total = 0;
function addToTotal(value) {
total += value;
}
This modifies external state.
Common side effects
Network request
Database write
DOM mutation
console.log
localStorage
Changing global variables
Changing function arguments
Reading current time
Generating random values
Example:
function saveUser(user) {
localStorage.setItem('user', JSON.stringify(user));
}
Why pure functions are useful
Pure functions are easier to:
Test
Cache
Compose
Debug
Parallelize
Reason about
React connection
Bad:
function User({ user }) {
user.name = user.name.toUpperCase();
return <div>{user.name}</div>;
}
Better:
function User({ user }) {
const name = user.name.toUpperCase();
return <div>{name}</div>;
}
6. Immutability
Immutability means creating new data instead of modifying existing data.
Bad:
const user = {
name: 'Khanh',
age: 30,
};
user.age = 31;
Better:
const updatedUser = {
...user,
age: 31,
};
Arrays
Bad:
const items = [1, 2, 3];
items.push(4);
Better:
const updatedItems = [...items, 4];
Updating an item
const updatedUsers = users.map((user) =>
user.id === targetId
? {
...user,
active: true,
}
: user
);
Removing an item
const updatedUsers = users.filter((user) => user.id !== targetId);
Why React cares about immutability
React often relies on reference comparison:
oldState === newState;
Bad:
state.user.name = 'Bob';
setState(state);
Better:
setState({
...state,
user: {
...state.user,
name: 'Bob',
},
});
Shallow copy gotcha
Spread syntax performs a shallow copy.
const user = {
name: 'Khanh',
address: {
city: 'San Jose',
},
};
const nextUser = {
...user,
};
user.address and nextUser.address still reference the same nested object.
Correct nested update:
const nextUser = {
...user,
address: {
...user.address,
city: 'Seattle',
},
};
7. Functional Programming Patterns
Common JavaScript functional concepts:
Pure functions
Immutability
Higher-order functions
Function composition
map
filter
reduce
Closures
Currying
Declarative programming
Higher-order functions
A higher-order function accepts a function, returns a function, or both.
function calculate(a, b, operation) {
return operation(a, b);
}
const add = (a, b) => a + b;
calculate(2, 3, add); // 5
map
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => number * 2);
// [2, 4, 6]
filter
const numbers = [1, 2, 3, 4];
const even = numbers.filter((number) => number % 2 === 0);
// [2, 4]
reduce
const numbers = [1, 2, 3, 4];
const total = numbers.reduce((sum, number) => sum + number, 0);
// 10
Real interview example
const users = [
{ name: 'A', active: true, score: 10 },
{ name: 'B', active: false, score: 20 },
{ name: 'C', active: true, score: 30 },
];
const total = users
.filter((user) => user.active)
.map((user) => user.score)
.reduce((sum, score) => sum + score, 0);
console.log(total); // 40
Function composition
const trim = (value) => value.trim();
const lower = (value) => value.toLowerCase();
const normalize = (value) => {
return lower(trim(value));
};
normalize(' HELLO '); // "hello"
Currying
const add = (a) => (b) => a + b;
const add10 = add(10);
add10(5); // 15
add10(20); // 30
Imperative vs declarative
Imperative:
const result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
result.push(users[i].name.toUpperCase());
}
}
Declarative:
const result = users.filter((user) => user.active).map((user) => user.name.toUpperCase());
Interview Gotchas
Gotcha 1 — Event Loop
console.log('start');
Promise.resolve().then(() => {
console.log('promise');
});
setTimeout(() => {
console.log('timeout');
}, 0);
console.log('end');
Output:
start
end
promise
timeout
Gotcha 2 — Hoisting
var x = 10;
function test() {
console.log(x);
var x = 20;
}
test();
Output:
undefined
Gotcha 3 — Lexical Scope
let x = 1;
function outer() {
let x = 2;
return function inner() {
console.log(x);
};
}
const fn = outer();
fn();
Output:
2
Gotcha 4 — Promise chaining
Promise.resolve(1)
.then((x) => x + 1)
.then((x) => {
console.log(x);
return x * 2;
})
.then(console.log);
Output:
2
4
Gotcha 5 — React state mutation
Bad:
const state = {
users: [],
};
state.users.push(newUser);
setState(state);
Better:
setState({
...state,
users: [...state.users, newUser],
});
60-Second Review
CLOSURE
Function remembers lexical environment.
EVENT LOOP
sync -> microtasks -> next task.
PROMISE CHAINING
.then() returns a new Promise.
Always return async work from .then().
SCOPE
JavaScript uses lexical scope.
HOISTING
var -> undefined
let/const -> TDZ
function declaration -> callable early
PURE FUNCTION
same input -> same output
no side effects
IMMUTABILITY
create new values instead of mutating old ones
FUNCTIONAL PATTERNS
map = transform
filter = select
reduce = aggregate
higher-order = takes/returns functions
composition = combine small functions
currying = function returning partially configured function
Senior / Staff Interview Framing
Connect the fundamentals to production engineering:
Closures
-> hooks, callbacks, stale closures, event handlers
Event loop
-> UI responsiveness, long tasks, microtask starvation
Promises
-> concurrency, error handling, cancellation
Scope
-> module boundaries and state ownership
Pure functions
-> testable business logic and reducers
Immutability
-> React rendering, memoization, state management
Functional patterns
-> reusable transformations and predictable pipelines
A strong answer moves from:
language concept
|
v
small example
|
v
common bug
|
v
production implication