Episode 1 — Fundamentals / 1.20 — Functions
Interview Questions: Functions in JavaScript
Model answers for declarations vs expressions, hoisting, closures, scope, arrow functions, higher-order functions, recursion, and real-world patterns.
How to use this material (instructions)
- Read lessons in order —
README.md, then1.20.a→1.20.g. - Practice out loud — definition → example → pitfall.
- Pair with exercises —
1.20-Exercise-Questions.md. - Quick review —
1.20-Quick-Revision.md.
Beginner (Q1–Q5)
Q1. What is the difference between a function declaration and a function expression?
Why interviewers ask: Tests fundamental syntax knowledge and understanding of hoisting.
Model answer:
A function declaration uses the function keyword followed by a name (function greet() {}) and is fully hoisted — you can call it before the line it appears on. A function expression assigns a function (named or anonymous) to a variable (const greet = function() {}). The variable follows normal hoisting rules: const/let throw a ReferenceError if accessed before declaration (TDZ), and var is hoisted as undefined (calling it gives TypeError). Use declarations for standalone helpers; use expressions for callbacks, conditional creation, or when you want to prevent calling before definition.
// Works — declaration is hoisted
sayHi();
function sayHi() { console.log("Hi"); }
// ReferenceError — expression not hoisted
// sayBye();
const sayBye = function () { console.log("Bye"); };
Q2. What are default parameters and rest parameters?
Why interviewers ask: ES6 fundamentals every developer should know.
Model answer:
Default parameters provide fallback values when an argument is undefined or not passed: function greet(name = "World"). They are evaluated left-to-right and can reference earlier parameters or call functions.
Rest parameters (...args) collect all remaining arguments into a real Array. They must be the last parameter. They replace the legacy arguments object, which is array-like (not a real Array) and unavailable in arrow functions.
function log(level, ...messages) {
console.log(`[${level}]`, ...messages);
}
log("INFO", "Server started", "Port 3000");
// [INFO] Server started Port 3000
Q3. Explain scope in JavaScript.
Why interviewers ask: Scope bugs are extremely common; understanding prevents them.
Model answer:
JavaScript has three scope levels: global (accessible everywhere), function (variables declared inside a function), and block (variables declared with let/const inside {} — if, for, etc.). var is function-scoped only (ignores block boundaries). JavaScript uses lexical scoping — a function's variable access is determined by where it is written in the source, not where it is called. When a variable is referenced, the engine walks the scope chain — current scope, then parent, then grandparent, up to global. If not found: ReferenceError.
Q4. What does it mean that functions are "first-class citizens"?
Why interviewers ask: Foundation for understanding callbacks, HOFs, and functional patterns.
Model answer:
In JavaScript, functions are values — just like numbers, strings, or objects. This means you can: (1) assign a function to a variable, (2) pass a function as an argument to another function (callbacks), (3) return a function from a function (closures, factories), and (4) store functions in arrays or objects. This first-class nature enables patterns like higher-order functions (map, filter, reduce), the module pattern, middleware, and functional composition.
Q5. What is the difference between return and no return?
Why interviewers ask: Simple but reveals attention to detail.
Model answer:
return does two things: sends a value back to the caller and immediately exits the function. If a function has no return statement, or uses return; with no value, it returns undefined. This matters when chaining: const result = myFunc() will be undefined if myFunc does not explicitly return. A common bug is writing forEach where you meant map — forEach always returns undefined.
Intermediate (Q6–Q10)
Q6. What is a closure? Give a practical example.
Why interviewers ask: Closures are a core JavaScript concept — essential for interviews.
Model answer:
A closure is a function bundled together with references to its surrounding lexical environment. When an inner function is returned from an outer function, it retains access to the outer function's variables even after the outer function has finished executing.
Practical example — private counter:
function createCounter() {
let count = 0; // private — not accessible outside
return {
increment() { return ++count; },
getCount() { return count; },
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2
// count is not accessible — truly private
Closures power the module pattern, data encapsulation, function factories (e.g., createMultiplier(2) returns a function that doubles), memoization, and event handlers that remember state.
Q7. Explain the this difference between arrow functions and regular functions.
Why interviewers ask: One of the most common sources of bugs, especially in event handlers and class methods.
Model answer:
Regular functions get their this from how they are called — method call (obj.fn() → this = obj), standalone call (fn() → this = undefined in strict mode / global in sloppy), or new (this = new instance).
Arrow functions have no own this. They inherit this from the enclosing lexical scope at definition time. This means:
- Arrow functions are great for callbacks inside methods (e.g.,
setTimeout,.map) because they keep the method'sthis. - Arrow functions are bad as object methods because
thiswould be the outer scope (oftenundefinedorwindow), not the object. - Arrow functions cannot be used with
new.
const obj = {
name: "Alice",
regular() { console.log(this.name); }, // "Alice"
arrow: () => { console.log(this.name); }, // undefined (outer scope)
};
Q8. Explain the classic var loop closure bug and how to fix it.
Why interviewers ask: Classic JS interview question — tests closure + scope understanding.
Model answer:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3
var is function-scoped, so all three callbacks share the same i. By the time setTimeout fires, the loop has finished and i is 3.
Fix 1 (modern): Use let — it is block-scoped, so each iteration gets its own i:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2
Fix 2 (IIFE): Create a new scope each iteration:
for (var i = 0; i < 3; i++) {
((captured) => setTimeout(() => console.log(captured), 100))(i);
}
Q9. What is a higher-order function? Give examples.
Why interviewers ask: Foundation for functional programming patterns in JS.
Model answer:
A higher-order function (HOF) either (1) takes a function as an argument or (2) returns a function.
Built-in examples: Array.prototype.map(fn), .filter(fn), .reduce(fn, init), .sort(compareFn), .forEach(fn), setTimeout(fn, delay).
Custom example — function factory:
function withLogging(fn) {
return function (...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// "Calling add with [2, 3]"
// "Result: 5"
Q10. What is the difference between passing a primitive and passing an object to a function?
Why interviewers ask: Understanding mutation is critical for bug prevention.
Model answer:
JavaScript is always pass-by-value, but for objects, the "value" is a reference (memory address).
- Primitives (number, string, boolean, etc.): a copy of the value is passed. Changing the parameter inside the function does not affect the original.
- Objects (object, array, function): a copy of the reference is passed. Both the outer variable and the parameter point to the same object in memory. Mutating the object inside the function does affect the original. However, reassigning the parameter to a new object does not affect the outer variable.
function mutate(obj) { obj.x = 99; }
function reassign(obj) { obj = { x: 99 }; }
const a = { x: 1 };
mutate(a); // a.x is now 99 — mutated
reassign(a); // a.x is still 99 — reassignment did not affect outer
Advanced (Q11–Q15)
Q11. What is a pure function and why should you care?
Why interviewers ask: Core concept for maintainable, testable code.
Model answer:
A pure function satisfies two rules: (1) deterministic — same inputs always produce the same output, and (2) no side effects — it does not modify external state, make network calls, write to the DOM, or depend on anything other than its inputs.
Why care:
- Testable — just assert input/output, no mocking.
- Cacheable — results can be memoized.
- Predictable — easier to reason about in large codebases.
- Composable — safe to chain without worrying about order-dependent side effects.
Real-world code needs side effects (DOM updates, API calls). The strategy is: keep core logic pure and push side effects to the edges of the application.
Q12. Explain function composition and the pipe / compose patterns.
Why interviewers ask: Tests understanding of functional programming in JS.
Model answer:
Function composition combines small, single-purpose functions into a pipeline where the output of one becomes the input of the next.
pipe applies functions left-to-right (reads naturally):
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const process = pipe(trim, toLowerCase, slugify);
process(" Hello World "); // "hello-world"
compose applies functions right-to-left (mathematical order f(g(x))):
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
Both promote small, reusable, testable functions over large monolithic ones.
Q13. Explain recursion, the call stack, and when recursion is better than iteration.
Why interviewers ask: Recursion comes up in tree/graph problems and algorithm design.
Model answer:
Recursion is when a function calls itself. It requires a base case (to stop) and a recursive case (that moves toward the base case). Each call pushes a new frame onto the call stack; when the base case is reached, the stack unwinds as each frame returns its result.
Stack overflow occurs when recursion is too deep (no base case or input too large). The call stack has a finite size.
Recursion is better than iteration for naturally tree-shaped or nested data: DOM traversal, file system walking, deeply nested object operations (deep clone, flatten), divide-and-conquer algorithms (merge sort, quick sort). For flat sequences (summing numbers, factorial), iteration is usually faster and stack-safe.
Optimization: Memoization caches results to avoid redundant recursive calls (e.g., Fibonacci: O(2^n) → O(n)).
Q14. What is an IIFE and why was it important before ES modules?
Why interviewers ask: Understanding JS history explains patterns in legacy codebases.
Model answer:
An IIFE (Immediately Invoked Function Expression) is a function that runs the moment it is defined: (function() { ... })(). It creates a new scope, preventing variables from leaking into the global namespace.
Before ES modules (import/export), all scripts on a page shared one global scope. Libraries used IIFEs to avoid collisions:
const myLib = (function () {
let privateState = 0;
return {
increment() { return ++privateState; },
};
})();
With ES modules, each file has its own scope, so IIFEs are less necessary. They still appear in: bundler output, polyfills, async IIFE for top-level await in scripts, and initialization code that should run once.
Q15. Compare all function definition styles in JavaScript.
Why interviewers ask: Comprehensive knowledge check.
Model answer:
| Style | Syntax | Hoisted? | Own this? | Own arguments? | Constructable? |
|---|---|---|---|---|---|
| Declaration | function name() {} | Yes (fully) | Yes | Yes | Yes |
| Expression (anonymous) | const fn = function() {} | No | Yes | Yes | Yes |
| Expression (named) | const fn = function name() {} | No | Yes | Yes | Yes |
| Arrow | const fn = () => {} | No | No (lexical) | No | No |
| Method shorthand | { method() {} } | N/A (object) | Yes | Yes | No |
| Class method | class { method() {} } | N/A (class) | Yes | Yes | No |
Function constructor | new Function("a", "return a") | No | Yes | Yes | Yes |
General guidance: Use declarations for top-level helpers, arrow functions for callbacks and short utilities, method shorthand for object/class methods, and avoid the Function constructor.
Quick-fire
| # | Question | One-line answer |
|---|---|---|
| 1 | Declaration hoisted? | Yes — fully |
| 2 | Expression with const hoisted? | No — TDZ ReferenceError |
| 3 | Arrow function has own this? | No — lexical from enclosing scope |
| 4 | arguments in arrow function? | No — use rest params |
| 5 | ...args is a real array? | Yes |
| 6 | Passing object to function — mutation? | Affects original |
| 7 | Passing object — reassignment? | Does NOT affect original |
| 8 | Closure = ? | Function + its lexical environment |
| 9 | var loop + setTimeout output? | Same final value for all callbacks |
| 10 | Pure function rule? | Same input → same output, no side effects |
| 11 | IIFE purpose? | Scope isolation, immediate execution |
| 12 | Recursion needs? | Base case + recursive case |
| 13 | Stack overflow cause? | Missing or unreachable base case |
| 14 | null triggers default param? | No — only undefined does |
| 15 | Arrow function as constructor? | No — TypeError |
← Back to 1.20 — Functions (README)