JavaScript · Functions

Functions Interview Questions
With Answers & Code Examples

18 carefully curated Functions interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
18 questions6 beginner5 core7 advanced
Q1Core

What is the difference between call, apply, and bind?

💡 Hint: All set "this" — call=comma, apply=array, bind=returns new fn

All three explicitly set this:

  • call(thisArg, arg1, arg2) — invoke immediately, args individually
  • apply(thisArg, [args]) — invoke immediately, args as array
  • bind(thisArg, arg1) — returns new permanently-bound function
function greet(greeting, punct) {
  return `${greeting}, ${this.name}${punct}`;
}
const user = { name: 'Priya' };
greet.call(user, 'Hello', '!');     // "Hello, Priya!"
greet.apply(user, ['Hi', '.']);     // "Hi, Priya."
const fn = greet.bind(user, 'Hey');
fn('?');                            // "Hey, Priya?"
💡 Call=Comma, Apply=Array, Bind=returns Bound fn
Practice this question →
Q2Beginner

How do arrow functions differ from regular functions?

💡 Hint: No own this, no arguments object, no new, no prototype

Arrow functions are not just shorter syntax — key behavioral differences:

  • No own this — inherits lexical this from outer scope
  • No arguments object — use rest params (...args)
  • Cannot be constructors — new throws TypeError
  • No prototype property
const obj = {
  name: 'Dev',
  regular() { console.log(this.name); },  // 'Dev'
  arrow: () => console.log(this.name),    // undefined
};
💡 Use arrow fns for callbacks (inherit this). Use regular fns for methods and constructors.
Practice this question →
Q3Core

What is a pure function and why does it matter?

💡 Hint: Same input → same output, no side effects

A pure function: (1) always returns the same output for same inputs, (2) has zero side effects.

// Pure ✅
const add = (a, b) => a + b;

// Impure ❌ — modifies external state
let total = 0;
const addToTotal = (n) => { total += n; return total; };

Pure functions are predictable, testable, and cacheable. React expects components and reducers to be pure.

Practice this question →
Q4Beginner

What are Higher-Order Functions (HOF)?

💡 Hint: Functions that take other functions as arguments, or return functions as results

A Higher-Order Function is a function that either:

  • Accepts a function as an argument, OR
  • Returns a function as its result (or both)
// Takes a function as argument
[1, 2, 3].map(x => x * 2);        // map is HOF — takes callback
[1, 2, 3].filter(x => x > 1);     // filter is HOF
setTimeout(() => console.log('hi'), 1000); // HOF

// Returns a function
function multiplier(factor) {
  return (n) => n * factor; // returns a new function
}
const double = multiplier(2);
const triple = multiplier(3);
double(5); // 10
triple(5); // 15

// Does both (debounce)
function debounce(fn, delay) {
  let timer;
  return (...args) => {          // returns function
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay); // takes fn
  };
}

HOFs are foundational to functional programming, enabling code reuse, composition, and abstractions without mutation.

💡 map, filter, reduce, forEach, addEventListener, setTimeout — all HOFs you use every day without realizing it.
Practice this question →
Q5Core

What is an IIFE (Immediately Invoked Function Expression) and when do you use it?

💡 Hint: Defined and called immediately — creates a private scope

An IIFE is a function that is both defined and invoked immediately. It creates an isolated scope.

// Classic IIFE syntax
(function() {
  const private = 'inaccessible outside';
  console.log(private);
})();

// Arrow IIFE
(() => {
  // isolated scope
})();

// IIFE with parameters
(function(global) {
  global.myLib = {};
})(window);

// IIFE returning a value
const result = (() => {
  const x = computeExpensiveThing();
  return x * 2;
})();

Use cases:

  • Avoid polluting global scope (classic library pattern)
  • Create truly private variables (module pattern)
  • Capture loop variables (pre-let closure fix)
  • One-time initialization logic
💡 In modern JS, ES modules and block-scoped let/const make IIFEs less necessary. But they're heavily used in legacy code and still valid for specific patterns.
Practice this question →
Q6Beginner

What does it mean that functions are "first-class citizens" in JavaScript?

💡 Hint: Functions are values — assignable, passable, returnable, storable

Functions are first-class citizens — they're treated as values just like strings or numbers. This means:

  • Assign to variables
  • Pass as arguments
  • Return from functions
  • Store in arrays/objects
  • Have properties and methods attached
// Assigned to variable
const greet = (name) => 'Hello, ' + name;

// Passed as argument (callback)
[1, 2, 3].forEach(function(n) { console.log(n); });

// Returned from function
function makeAdder(x) {
  return (y) => x + y; // ← function as return value
}
const add5 = makeAdder(5);
add5(3); // 8

// Stored in object
const math = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
};

// Has properties
function fn() {}
fn.version = '1.0';
console.log(fn.name);   // 'fn'
console.log(fn.length); // 0 (param count)
💡 First-class functions are what enable HOFs, callbacks, closures, and all functional programming patterns in JS.
Practice this question →
Q7Advanced

What is currying and how do you implement a generic curry function?

💡 Hint: Transform f(a,b,c) into f(a)(b)(c) — each call returns a new function waiting for more args

Currying transforms a multi-argument function into a chain of unary functions, each waiting for one argument at a time.

// Manual curried function
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6
const add1 = add(1);     // partially applied — waits for b and c
const add1and2 = add1(2); // waits for c
add1and2(3);              // 6

// Generic curry utility
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) { // enough args collected?
      return fn(...args);
    }
    return (...moreArgs) => curried(...args, ...moreArgs);
  };
}

const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6 — also works (partial application hybrid)

Currying enables: partial application, point-free style, composable specialized functions.

💡 Currying vs Partial Application: currying always breaks a function into unary steps. Partial application pre-fills SOME arguments and returns a function waiting for the rest.
Practice this question →
Q8Advanced

What is memoization and how do you implement it?

💡 Hint: Cache results keyed by arguments — avoid recomputing for the same inputs

Memoization is an optimization where a function caches its results. Calling with the same inputs returns the cached result instead of recomputing.

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key); // cache hit
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Fibonacci without memoization: O(2^n)
// With memoization: O(n)
const fib = memoize(function(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2); // self-referencing
});

fib(40); // instant — 40 cache entries
fib(40); // instant again — cache hit

When to use: Pure functions with expensive computation and repeated same-argument calls. React's useMemo and useCallback implement this concept.

💡 Only memoize PURE functions — same input must always give same output. Don't memoize time-dependent or side-effectful functions.
Practice this question →
Q9Advanced

What is function composition and how do compose() and pipe() differ?

💡 Hint: Chain functions: output of one becomes input of next — compose=right-to-left, pipe=left-to-right

Function composition combines multiple functions where the output of one becomes the input of the next, building complex operations from simple pieces.

// compose — right to left (mathematical convention)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

// pipe — left to right (more readable)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const trim      = str => str.trim();
const lowercase = str => str.toLowerCase();
const addBang   = str => str + '!';

// compose: addBang(lowercase(trim(x)))
const processC = compose(addBang, lowercase, trim);

// pipe: trim → lowercase → addBang
const processP = pipe(trim, lowercase, addBang);

processP('  Hello World  '); // 'hello world!'
processC('  Hello World  '); // 'hello world!'

// Without composition (harder to read as chain grows)
const manual = str => addBang(lowercase(trim(str)));
💡 compose() mirrors mathematical f∘g notation. pipe() reads like a Unix pipeline — more natural for most developers. Both are equivalent, just different argument order.
Practice this question →
Q10Core

What are rest parameters and how do they differ from the arguments object?

💡 Hint: Rest (...args) is a real Array; arguments is array-like, no arrow support, no Array methods

Rest parameters (...args) collect remaining function arguments into a real Array.

// Rest parameters — modern
function sum(first, ...rest) {
  console.log(first);   // 1
  console.log(rest);    // [2, 3, 4] — real Array!
  return rest.reduce((a, b) => a + b, first);
}
sum(1, 2, 3, 4); // 10
rest.map(x => x * 2); // ✅ has Array methods

// arguments — legacy
function old() {
  console.log(arguments);        // { 0:1, 1:2, ... } — array-LIKE
  console.log(arguments.map);    // undefined — NOT a real Array
  const arr = Array.from(arguments); // convert needed
}

// Arrow functions have NO arguments object
const arrow = () => {
  console.log(arguments); // ReferenceError!
  // Use rest: (...args) => { console.log(args) }
};

Key differences:

  • Rest is a real Array → has all array methods
  • arguments is array-like → no map/filter/etc
  • Arrow functions don't have arguments
  • Rest collects only the remaining args after named params
💡 Always use rest parameters in modern code. arguments is legacy and has quirks that trip people up.
Practice this question →
Q11Core

What is recursion and what causes a stack overflow?

💡 Hint: Function calling itself — needs a base case; too many calls = call stack exhausted

Recursion is when a function calls itself. Every recursive function needs:

  1. A base case — a stopping condition
  2. A recursive case — that moves toward the base case
// Factorial
function factorial(n) {
  if (n <= 1) return 1;         // base case
  return n * factorial(n - 1); // recursive case
}
factorial(5); // 120

// Flatten nested array
function flatten(arr) {
  return arr.reduce((acc, item) =>
    Array.isArray(item)
      ? acc.concat(flatten(item)) // recurse
      : acc.concat(item),
  []);
}
flatten([1, [2, [3]]]); // [1, 2, 3]

Stack overflow: Each recursive call adds a stack frame. Without a base case (or with very deep recursion), the call stack fills up:

function infinite(n) {
  return infinite(n + 1); // no base case!
}
infinite(1); // RangeError: Maximum call stack size exceeded
💡 Tail Call Optimization (TCO) can theoretically prevent stack overflow for tail-recursive calls, but TCO is only reliably supported in Safari. For deep recursion, use iteration or trampolining.
Practice this question →
Q12Advanced

What is a Named Function Expression (NFE)?

💡 Hint: A function expression with an internal name — visible inside the body only

A Named Function Expression has a name after function, but unlike a declaration, the name is only visible inside the function body — not outside.

// Anonymous function expression — self-reference is fragile
const factorial = function(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // relies on outer var 'factorial'
};
// If factorial is reassigned, self-reference breaks!

// Named function expression — safe self-reference
const factorial = function fact(n) {
  if (n <= 1) return 1;
  return n * fact(n - 1); // 'fact' is always THIS function
};

console.log(factorial.name); // 'fact'
console.log(typeof fact);    // 'undefined' — not accessible outside!

// If outer var is reassigned, NFE still works
const f = factorial;
factorial = null;
f(5); // 120 — 'fact' still refers to itself correctly
💡 Use NFEs for recursive function expressions. Better stack traces (shows 'fact' not 'anonymous') and reliable self-reference even if the outer variable changes.
Practice this question →
Q13Advanced

What are the Module Pattern and the Revealing Module Pattern?

💡 Hint: IIFE + closure = private scope; expose only public API; revealing = explicitly name what's public

Pre-ES-modules patterns for creating encapsulated, private state in JavaScript.

// Module Pattern
const counter = (function() {
  let _count = 0; // private — inaccessible from outside

  return {
    increment() { _count++; },
    decrement() { _count--; },
    getCount()  { return _count; }
  };
})();

counter.increment();
counter.getCount(); // 1
counter._count;     // undefined — truly private ✓

// Revealing Module Pattern
// Define everything privately, then reveal selectively
const bankAccount = (function() {
  let _balance = 1000;
  let _transactions = [];

  function _log(type, amount) {
    _transactions.push({ type, amount, date: Date.now() });
  }

  function deposit(amount) {
    if (amount > 0) { _balance += amount; _log('deposit', amount); }
  }

  function withdraw(amount) {
    if (amount > 0 && amount <= _balance) {
      _balance -= amount; _log('withdrawal', amount);
    }
  }

  function getBalance() { return _balance; }
  function getHistory() { return [..._transactions]; }

  // Explicitly reveal the public interface
  return { deposit, withdraw, getBalance, getHistory };
})();
💡 Before ES modules, this was THE pattern for encapsulation. jQuery, Lodash, and most pre-2015 JS libraries used this. Today, use ES modules instead.
Practice this question →
Q14Advanced

What is partial application and how does it differ from currying?

💡 Hint: Partial application = pre-fill some args, return function waiting for the rest; currying = always one arg at a time

Both techniques create specialized functions from general ones — but differ in how arguments are collected.

// Partial Application — pre-fill SOME args
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

const add = (a, b, c) => a + b + c;
const add10 = partial(add, 10);         // pre-fill first arg
const add10and20 = partial(add, 10, 20); // pre-fill two args

add10(5, 3);     // 18 — takes remaining 2 args AT ONCE
add10and20(7);   // 37 — takes remaining 1 arg

// Currying — always ONE arg at a time
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3); // 6 — strictly one at a time

// Practical partial application with bind()
function greet(greeting, punct, name) {
  return `${greeting}, ${name}${punct}`;
}
const hello = greet.bind(null, 'Hello', '!'); // partial via bind
hello('Alice'); // 'Hello, Alice!'
hello('Bob');   // 'Hello, Bob!'

Summary:

  • Currying: f(a, b, c) → f(a)(b)(c) — each call takes exactly ONE argument
  • Partial application: f(a, b, c) → f(a, b)(c) — pre-fill any number of args
💡 In practice, curried functions support partial application too (you can call with multiple args and they accumulate). The distinction is mostly theoretical.
Practice this question →
Q15Beginner

What is the difference between function declarations and function expressions?

💡 Hint: Declarations are hoisted fully; expressions are not — and expression form gives more control

Both create functions but behave differently with hoisting and syntax.

// Function Declaration — hoisted completely (name + body)
greet(); // ✅ works BEFORE the declaration
function greet() { return 'hello'; }

// Function Expression — NOT hoisted as a function
sayHi(); // ❌ TypeError: sayHi is not a function (var hoisted as undefined)
var sayHi = function() { return 'hi'; };

// Arrow function expression
const add = (a, b) => a + b;

// Named function expression (NFE) — name only visible inside
const fact = function factorial(n) {
  return n <= 1 ? 1 : n * factorial(n - 1); // factorial = self
};
console.log(typeof factorial); // undefined — not accessible outside

When to prefer each:

  • Declaration — top-level utility functions, when you want full hoisting
  • Expression — callbacks, conditional function creation, storing in variables, passing as args
💡 Most style guides prefer expressions (especially arrow) for callbacks and class methods. Declarations are fine for named utility functions at module scope.
Practice this question →
Q16Beginner

What is the spread operator (...) and what are its use cases?

💡 Hint: Expand iterable into individual elements — arrays, function args, object spreading
// 1. Spread in function calls — expand array as arguments
const nums = [1, 5, 3, 2, 4];
Math.max(...nums); // 5 — same as Math.max(1,5,3,2,4)

// 2. Copy and combine arrays (immutable operations)
const a = [1, 2, 3];
const b = [4, 5, 6];
const copy    = [...a];          // [1,2,3] — shallow copy
const merged  = [...a, ...b];    // [1,2,3,4,5,6]
const prepend = [0, ...a];       // [0,1,2,3]

// 3. Spread in object literals (ES2018)
const base = { a: 1, b: 2 };
const extended = { ...base, c: 3 };        // { a:1, b:2, c:3 }
const override = { ...base, b: 99 };       // { a:1, b:99 } — later wins

// 4. Convert iterable to array
const set = new Set([1,2,3]);
[...set]; // [1,2,3]
[...'hello']; // ['h','e','l','l','o']
[...document.querySelectorAll('p')]; // NodeList → Array

// 5. Clone + update (immutable pattern)
const state = { user: 'Alice', count: 0 };
const newState = { ...state, count: state.count + 1 };
💡 Spread creates SHALLOW copies — nested objects are still shared references. Use structuredClone() for deep copies. Spread in objects is order-sensitive: later properties win.
Practice this question →
Q17Beginner

What are default parameters and how do they work?

💡 Hint: Evaluated at call time, only when arg is undefined — can reference earlier params and outer scope
// Basic default parameters
function greet(name = 'World', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}
greet();              // 'Hello, World!'
greet('Alice');       // 'Hello, Alice!'
greet('Bob', 'Hi');   // 'Hi, Bob!'
greet(undefined, 'Hey'); // 'Hey, World!' — undefined triggers default
greet(null, 'Hey');   // 'Hey, null!' — null does NOT trigger default

// Defaults can reference earlier parameters
function range(start = 0, end = start + 10) {
  return { start, end };
}
range();     // { start: 0, end: 10 }
range(5);    // { start: 5, end: 15 }

// Defaults can be expressions / function calls
let count = 0;
function makeId(id = ++count) { return id; } // evaluated each call
makeId(); // 1
makeId(); // 2
makeId(99); // 99 (provided, so default not evaluated)

// Defaults + destructuring (very common pattern)
function createUser({ name = 'Anonymous', role = 'user', active = true } = {}) {
  return { name, role, active };
}
createUser({ name: 'Alice' }); // { name:'Alice', role:'user', active:true }
createUser();                  // {} → uses = {} → all defaults apply
💡 Default params replaced the old pattern: name = name || 'World'. The old way was buggy (falsy values like 0 or '' triggered the default). New defaults only trigger for undefined.
Practice this question →
Q18Advanced

What is tail call optimization (TCO) and how does it prevent stack overflow?

💡 Hint: A tail call is the last operation in a function — engine can reuse the stack frame instead of adding a new one

A tail call is when the last action of a function is calling another function. If the engine applies TCO, it reuses the current stack frame instead of pushing a new one — preventing stack overflow for deep recursion.

// Regular recursion — O(n) stack frames
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // NOT tail call — must multiply AFTER return
}
factorial(100000); // Stack overflow!

// Tail-recursive version — last op is the call
function factTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factTail(n - 1, n * accumulator); // tail call — nothing after it
}
// With TCO, this would run in O(1) stack space

// Current reality:
// TCO is in the ES6 spec BUT only Safari implements it fully
// V8 (Node/Chrome) removed their TCO implementation
// So tail recursion is NOT safe in Node.js / Chrome!

// Practical alternatives:
// 1. Iteration (always safe)
function factIterative(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) result *= i;
  return result;
}

// 2. Trampolining — simulates TCO in user space
const trampoline = fn => (...args) => {
  let result = fn(...args);
  while (typeof result === 'function') result = result();
  return result;
};

const factTramp = trampoline(function fact(n, acc = 1) {
  return n <= 1 ? acc : () => fact(n - 1, n * acc); // return fn instead of calling
});
factTramp(100000); // works!
💡 Know the theory for interviews but use iteration in production. Trampolining is the practical way to handle very deep recursion in JS without relying on TCO.
Practice this question →

Other JavaScript Interview Topics

Core JSAsync JSObjectsArrays'this' KeywordError HandlingModern JSPerformanceDOM & EventsBrowser APIs

Ready to practice Functions?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →