DEV Community

Cover image for 12 JavaScript Interview Questions Every Dev Should Know — Part 2: Functions, Scope & Closures
Keyur Gohil
Keyur Gohil

Posted on Edited on

12 JavaScript Interview Questions Every Dev Should Know — Part 2: Functions, Scope & Closures

Welcome to Part 2 of the JS interview series! This time we're tackling functions, scope, and the topic that trips up even experienced developers in interviews: closures.

Missed Part 1? Check out Fundamentals & Data Types first.


Q1. What is a closure?

A closure is formed whenever a function is created: every function in JavaScript automatically retains a live link to the lexical scope in which it was defined, and can continue to access the variables in that scope for as long as the function itself is reachable — even after the code that originally created that scope has finished running. This isn't a special or occasional behavior; it's a fundamental property of every single function, since JavaScript uses lexical (not dynamic) scoping.

A common but inaccurate way this gets described is "an inner function that remembers its outer function's variables." That's the most useful and most commonly demonstrated case, but nesting one function inside another isn't actually a requirement for a closure to exist. Even a top-level function that references a variable from the global scope is technically forming a closure over that scope:

let x = 10;
function notNested() {
  console.log(x); // still a closure — closing over the global scope
}
Enter fullscreen mode Exit fullscreen mode

What makes nested closures specifically powerful and interview-relevant is that they let an inner function keep access to an outer function's local variables — variables that would otherwise be destroyed once the outer function returns. That's what enables patterns like private state, factory functions, and memoization:

function counter() {
  let count = 0;
  return () => ++count;
}
const inc = counter();
inc(); // 1
inc(); // 2
Enter fullscreen mode Exit fullscreen mode

Here, count would normally be garbage collected once counter() finishes executing — but because the returned arrow function closes over it, count stays alive in memory, and there's no way to reach it from outside except through inc().

Q2. What is lexical scoping?

Lexical scoping (also called static scoping) means that a variable's accessibility is determined entirely by where it's physically written in your source code — not by which function called which, or the order in which functions happen to execute at runtime. When JavaScript compiles your code, it can already determine, just by looking at the nesting of functions and blocks, exactly which variables any given piece of code will be able to see.

This is what allows an inner function to "reach out" to variables declared in the function that contains it, and that function's own container, all the way up to the global scope — a chain often called the scope chain. Because this resolution happens based on code structure rather than call order, the same function will always resolve its outer variables the same way, regardless of where or how many times it's invoked, which is exactly why closures behave predictably.

Q3. What is the difference between function declarations and function expressions?

A function declaration is a function defined as a standalone statement using the function keyword, given a name, and not used as part of any larger expression — for example, function greet() {} written on its own line. Declarations are hoisted completely by the JavaScript engine, meaning both the function's name and its entire body are available before execution even reaches that line in the code, which is why you can call a declared function from earlier in the file, before its definition physically appears.

A function expression is a function that appears as part of an expression, rather than as its own statement — meaning it's defined somewhere a value is expected. Assigning a function to a variable (const greet = function() {}) is the most common example, but it's important not to conflate "function expression" with "assigned to a variable" — those are two different things. A function can be a function expression without ever being assigned to a variable at all, such as when it's passed directly as a callback argument or immediately invoked:

setTimeout(function () { console.log("hi"); }, 1000); // function expression, no variable
(function () { console.log("IIFE"); })();               // function expression, no variable
const greet = function () { console.log("hello"); };     // function expression, assigned to a variable
Enter fullscreen mode Exit fullscreen mode

What actually matters for hoisting behavior is only whether it was assigned to a variable, and if so, which kind. If a function expression is assigned to a var, only the variable name is hoisted (initialized to undefined); if assigned to let/const, the variable is hoisted into the Temporal Dead Zone. Either way, the function itself only becomes callable once that assignment line actually executes — unlike a function declaration, whose entire body is available immediately.

Q4. What are arrow functions, and how do they differ from regular functions?

Arrow functions, introduced in ES6, offer a shorter syntax for writing functions, but the syntax isn't the main reason they matter in interviews — it's their fundamentally different behavior around this. A regular function gets its own this value determined dynamically by how it's called (as a method, standalone, with new, etc.), while an arrow function has no this of its own at all — it simply looks up and uses whatever this value exists in its surrounding lexical scope at the time it was defined.

This makes arrow functions especially well-suited for callbacks where you want to preserve the outer this (like inside a class method's event handler), but poorly suited for object methods or constructors where dynamic this binding is actually needed. Arrow functions also lack their own arguments object, super, and new.target, and they cannot be used as constructors — attempting new someArrowFn() throws a TypeError because arrow functions have no internal [[Construct]] method and no prototype property.

Q5. What is the this keyword, and how is its value determined?

this is a special keyword whose value is determined dynamically based on how a function is invoked, not where the function is physically defined in the code (the one major exception being arrow functions, which ignore this rule entirely and inherit this lexically). This dynamic nature is one of the most confusing aspects of JavaScript for developers coming from languages with more rigid this/self semantics.

There's a well-defined precedence order the engine follows to resolve this for any given function call:

  1. new binding — if the function is called with new, this refers to the newly created object.
  2. Explicit binding — if call(), apply(), or bind() was used to set this directly.
  3. Implicit binding — if the function was called as a method on an object (obj.method()), this refers to that object.
  4. Default binding — if none of the above apply, this falls back to the global object in non-strict mode, or undefined in strict mode (which is the default inside ES modules and classes).

Q6. What is the difference between call(), apply(), and bind()?

All three methods exist on every function and let you explicitly control what this refers to when the function runs, overriding whatever the default binding rules would otherwise produce. The key differences are in how arguments are passed and when the function actually executes.

call(thisArg, arg1, arg2, ...) invokes the function immediately, with any additional arguments passed individually, comma-separated, exactly as you'd normally call the function. apply(thisArg, [argsArray]) also invokes immediately, but expects its arguments bundled into a single array — useful when you already have an array of arguments (or historically, before the spread operator existed, for things like Math.max.apply(null, numbersArray)). bind(thisArg, ...) is different in kind: it does not call the function at all — instead, it returns a brand-new function with this permanently locked to the given value, which you can then invoke later, as many times as you like, always with that same this.

Q7. What is currying in JavaScript?

Currying is a functional programming technique where a function that logically takes several arguments is restructured into a chain of nested functions, each accepting exactly one argument, until all the arguments have been supplied and the original computation can run. Instead of calling add(1, 2, 3), a curried version is called as add(1)(2)(3), with each call returning a new function waiting for the next argument.

The real value of currying shows up with partial application — the ability to "lock in" some arguments early and produce a specialized, reusable function for later, without needing all the arguments up front. This is common in functional libraries and React/Redux-style code, for example creating a reusable multiplyByTwo = multiply(2) from a general-purpose curried multiply function, which can then be applied to many different values without repeating the first argument each time.

const add = a => b => c => a + b + c;
add(1)(2)(3); // 6
const add5 = add(5); // partially applied
Enter fullscreen mode Exit fullscreen mode

Q8. What is the arguments object?

arguments is a special, array-like object that's automatically available inside every regular (non-arrow) function, containing every argument that was actually passed to the function when it was called — regardless of how many parameters were formally declared in the function's signature. This made it historically useful for writing functions that accept a variable number of arguments, before rest parameters existed.

It's important to note that arguments is only array-like, not a true array: it has a length property and supports index-based access (arguments[0]), but it lacks array methods like .map(), .filter(), or .forEach() directly — you have to convert it first, typically with Array.from(arguments) or the spread operator [...arguments]. It's also completely unavailable inside arrow functions; if you reference arguments inside an arrow function, JavaScript looks it up in the nearest enclosing regular function instead, which is a subtle and common source of bugs.

Q9. What are default parameters?

Default parameters, introduced in ES6, let you specify a fallback value for a function parameter that will be used automatically if the caller either omits that argument entirely or explicitly passes undefined. Before this feature existed, developers had to manually check inside the function body (name = name || "Guest"), which had its own bugs since falsy-but-valid values like 0 or "" would incorrectly trigger the fallback.

function greet(name = "Guest") {
  return `Hello, ${name}`;
}
greet();        // "Hello, Guest"
greet("Sam");   // "Hello, Sam"
Enter fullscreen mode Exit fullscreen mode

Default parameter values aren't limited to simple literals — they can be arbitrary expressions, function calls, or even reference earlier parameters in the same parameter list, and they're evaluated fresh, at call time, every time the function runs without that argument being supplied.

Q10. What is a pure function?

A pure function is one that satisfies two strict guarantees: given the same input, it will always produce exactly the same output, with no exceptions or variation, and it causes no observable side effects anywhere else in the program — it doesn't mutate its arguments, doesn't modify any external or global state, and doesn't perform I/O like network requests, DOM updates, or console logging.

Pure functions are prized in functional programming because they're trivially easy to test (no setup or mocking of external state required), easy to reason about in isolation, safe to run in parallel or in any order, and safe to memoize (cache results for previously seen inputs) since their output is fully determined by their input. Impure functions — like one that reads Date.now(), reads/writes a global variable, or reassigns a property on an object passed in as an argument — break these guarantees and are harder to test and predict.

Q11. What is the difference between synchronous and asynchronous function execution regarding the call stack?

Synchronous code runs immediately, one operation at a time, directly on the call stack — each function call is pushed onto the stack, executes fully, and is popped off before the next line of code can run. This means a long-running synchronous operation (like a large loop or expensive computation) will completely block the rest of the program, including UI updates and user interactions, until it finishes.

Asynchronous operations work differently: rather than executing directly on the call stack, operations like setTimeout, network requests, or file reads are handed off to the browser's or Node's underlying runtime (Web APIs in the browser, libuv in Node), which handles them outside of JavaScript's single thread. Only once that underlying work completes does its associated callback get placed into a queue, and the event loop moves it onto the call stack for execution — but only after the call stack is completely empty, meaning all currently running synchronous code has finished first.

Q12. What is recursion, and what is a common pitfall with it in JavaScript?

Recursion is a technique where a function solves a problem by calling itself with a smaller or simpler version of that same problem, continuing until it reaches a "base case" — a condition simple enough to be answered directly without further recursive calls. Recursive solutions are often more elegant and readable than iterative ones for inherently recursive problems, such as traversing tree structures, computing factorials, or implementing certain sorting algorithms.

The major practical pitfall is the stack overflow error: every recursive call adds a new frame onto the call stack, and if the recursion goes too deep — either because the base case is missing, incorrect, or the input is simply very large — the stack will exceed its size limit and the program crashes. Many languages solve this with tail-call optimization, where the engine can reuse the current stack frame instead of adding a new one when a function's very last action is a recursive call, but despite tail-call optimization being part of the official ES6 specification, most JavaScript engines (including V8, which powers Chrome and Node.js) never actually implemented it — Safari's JavaScriptCore is the notable exception.


That's Part 2 done! Up next: Objects, Prototypes & OOP — prototypal inheritance, this, classes, and more.

Found this useful? Follow for the rest of the series and drop a comment with your favorite closure interview trick question 👇

Top comments (2)

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

A function expression — where a function is assigned to a variable

Function expression and assigning a function to a variable are two different things.

Also, nesting functions is not required for tge creation of a closure.

Collapse
 
keyurgohil13 profile image
Keyur Gohil

Good point, fixed both — thanks for catching that! 🙏