DEV Community

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

Posted on

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 what happens when an inner function "remembers" and continues to have access to the variables from its enclosing (outer) function's scope, even after that outer function has already finished running and would normally have had its local variables cleaned up. This works because JavaScript functions don't just capture the values of outer variables — they capture live references to them, keeping the entire surrounding scope alive in memory for as long as the inner function itself is reachable.

Closures are one of the most powerful and commonly used patterns in JavaScript. They're the mechanism behind data privacy (since variables inside a closure can't be accessed from outside except through the functions that were given access), factory functions that generate customized functions, memoization caches, and event handler callbacks that need to remember state from when they were created. In the classic counter example below, each call to counter() creates a fresh, independent count variable that only the returned function can see or modify — there's no way to reach into it from outside.

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

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 precisely what makes closures reliable and predictable.

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

A function declaration — written with the function keyword as a standalone statement, like function greet() {} — is hoisted completely by the JavaScript engine, meaning both its name and its entire body are available before execution even reaches that line in the code. This is why you're able to call a declared function from earlier in the file, before its definition physically appears.

A function expression — where a function is assigned to a variable, like const greet = function() {} or const greet = () => {} — behaves differently because only the variable declaration itself gets hoisted (following the normal var/let/const rules), while the function value isn't assigned to it until that line of code actually executes. Calling a function expression before its assignment line either throws a ReferenceError (with let/const, due to the TDZ) or gives you undefined is not a function (with var, since the variable exists but hasn't been assigned yet).

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 (1)

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.