Skip to main content

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