Skip to content

Latest commit

 

History

History
1298 lines (933 loc) · 45.4 KB

File metadata and controls

1298 lines (933 loc) · 45.4 KB

JavaScript Interview Prep — Functions, Closures & Currying

Source: Roadside Coder — JavaScript Interview Questions Course Topics: Function declarations/expressions, scope, hoisting, closures, lexical scope, currying, partial application, polyfills


Table of Contents

  1. Function Declaration vs Function Expression
  2. First-Class Functions
  3. IIFE — Immediately Invoked Function Expression
  4. Function Scope
  5. var vs let in Loops (Output-Based Question)
  6. Hoisting
  7. Function Hoisting vs Variable Hoisting
  8. Tricky Hoisting Output Question
  9. Params vs Arguments
  10. Spread vs Rest Operators
  11. Callback Functions
  12. Arrow Functions vs Regular Functions
  13. Closures — Introduction
  14. Lexical Scope
  15. Closures — Formal Definition and Examples
  16. Closure Scope Chain
  17. Closure Interview Question: Block Scope & Shadowing
  18. Closure Interview Question: Function Factory (Adder)
  19. Closure Interview Question: Optimizing Code with Closures
  20. Classic Interview Question: var, let, and setTimeout in Loops
  21. Fixing the var + Loop Problem Using Closures
  22. Closure Interview Question: Private Counter
  23. Module Pattern
  24. Closure Interview Question: Run Function Only Once (once Polyfill)
  25. Closure Interview Question: Memoization Polyfill
  26. Closure vs Scope
  27. Currying — Introduction
  28. Currying Interview Question: Sum of N Arguments
  29. Currying Interview Question: evaluate Function
  30. Currying Interview Question: Infinite Currying
  31. Currying vs Partial Application
  32. Real-World Use Case: Currying for DOM Manipulation
  33. Building a Generic curry Polyfill
  34. Key Terms Glossary

1. Function Declaration vs Function Expression

Function Declaration

function square(number) {
  return number * number
}
  • Also called a function definition or function statement.
  • Declared using the function keyword followed directly by a name.

Function Expression

const square = function (number) {
  return number * number
}
  • A function stored inside a variable.
  • The function itself (with no name) is called an anonymous function.
  • Anonymous functions can be:
    • Assigned to a variable, or
    • Passed as a callback (see Section 11).

Calling Either Form

console.log(square(5)) // 25
  • Both function declarations and function expressions are called the same way.
  • Key difference: function expressions are explicitly assigned to a variable; function declarations are not.

2. First-Class Functions

In a language where functions can be treated like variables, those functions are called first-class functions.

  • Functions can be:
    • Passed into other functions.
    • Manipulated.
    • Returned from other functions.
  • Essentially, everything a variable can do, a function can also do — this is why JavaScript functions are considered "first-class."

Example

function square(number) {
  return number * number
}

function displaySquare(fn, number) {
  console.log(`square is ${fn(number)}`)
}

displaySquare(square, 5) // "square is 25"
  • Here, square is passed into displaySquare just like a variable would be — demonstrating first-class function behavior.

3. IIFE — Immediately Invoked Function Expression

IIFE = Immediately Invoked Function Expression — a function that runs as soon as it is defined, without needing a separate call.

Syntax

;(function (number) {
  console.log(number * number)
})(5)
  • Wrap the function in parentheses, then immediately invoke it with another set of parentheses (optionally passing arguments).

Output-Based Question on IIFE (Closures Preview)

;(function () {
  var x = 1
  ;(function () {
    var y = 2
    console.log(x)
  })()
})()
  • Question: What does this print?
  • Common wrong guess: undefined (since x isn't defined in the inner scope).
  • Correct answer: 1.
  • Why: JavaScript searches for x in the inner scope first; not finding it, it searches the parent scope, where x = 1 exists. This behavior is due to closures (explained in detail later in this document).

4. Function Scope

Using the MDN documentation example:

const num1 = 20
const num2 = 30
const name = 'roadside coder'

function multiply() {
  return num1 * num2
}

console.log(multiply()) // 60
  • Since num1 and num2 are not defined inside multiply, the function looks them up in the global scope.

Shadowing Example

function getScore() {
  const num1 = 2
  const num2 = 3
  return `${name} scored ${num1 + num2}`
}

console.log(getScore()) // "roadside coder scored 5"
  • Even though num1 and num2 also exist in the global scope, the local num1/num2 inside the function shadow (override) the global versions within that function's scope.
  • name is still taken from the global scope, since it isn't redefined locally.

5. var vs let in Loops (Output-Based Question)

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 1000)
}
  • Output: 5, 5, 5, 5, 5 — because var is function-scoped, not block-scoped. By the time each setTimeout callback runs, the loop has already finished, and i holds its final value (5).
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 1000)
}
  • Output: 0, 1, 2, 3, 4 — because let is block-scoped, meaning each iteration of the loop creates a new block scope with its own separate copy of i.

This distinction is explained fully in Section 20.


6. Hoisting

  • Hoisting refers to variables/functions being conceptually "moved to the top" of their scope before code execution — but this happens differently depending on whether it's a variable or a function.
  • JavaScript code execution occurs in two phases:
    1. Initialization phase — the entire scope's variables and functions are scanned and set up first.
    2. Execution phase — the code actually runs, line by line.

Variable Hoisting Example

console.log(x) // undefined (not an error!)
var x = 5
console.log(x) // 5
  • var x is hoisted, but only the declaration, not the assignment — so accessing it before the assignment line gives undefined, not a ReferenceError.

7. Function Hoisting vs Variable Hoisting

sayHello() // works fine, even though called before declaration

function sayHello() {
  console.log('Hello!')
}
  • Functions declared with the function keyword are hoisted completely — the entire function body is copied to the top of the scope, not just its name.
  • This is different from variables, where only the declaration (not the value) is hoisted, resulting in undefined if accessed early.

Visualizing via DevTools

  • Inspecting the browser's Sources panel with a breakpoint at the top of a script shows:
    • Variables (e.g., x) hoisted as undefined.
    • Functions (e.g., sayHello) hoisted with their complete function definition already available.
  • This same hoisting behavior (complete initialization phase, then execution phase) also applies within function scopes, not just the global scope.

8. Tricky Hoisting Output Question

var x = 21

;(function () {
  console.log(x)
  var x = 20
})()

Question

What does console.log(x) print inside the IIFE — 21 (global) or 20 (local)?

Answer: undefined

Explanation

  • Hoisting is a two-step process:
    1. First, the global scope is initialized (x = 21).
    2. Then, when the IIFE runs, its own local scope is initialized — which hoists its own local var x declaration to the top of that function, setting it to undefined before the assignment (var x = 20) executes.
  • Because the local scope already has its own x (even though not yet assigned), JavaScript does not look up to the global scope — it uses the (currently undefined) local x.
  • Key takeaway: if a variable exists in the current scope (even if not yet assigned), JavaScript will never check the outer/global scope for that variable name — it will always refer to the current scope's version.

9. Params vs Arguments

function add(a, b) {
  // a and b are PARAMETERS (params)
  return a + b
}

add(5, 6) // 5 and 6 are ARGUMENTS
  • Parameters (params): the variable names listed in a function's definition.
  • Arguments: the actual values passed in when the function is called.

10. Spread vs Rest Operators

Spread Operator (...) — Expanding Values

function multiply(a, b) {
  return a * b
}

const nums = [5, 6]
console.log(multiply(...nums)) // 30
  • The ... operator here spreads (expands) the array elements into individual arguments.

Rest Operator (...) — Collecting Values

function multiply(...nums) {
  console.log(nums) // [5, 6]
  return nums[0] * nums[1]
}
  • Here, ...nums collects all passed arguments into a single array — called the rest operator.

Tricky Output Question: Rest Parameter Position

function example(x, y, z, ...numbers) {
  console.log(x, y, z)
  console.log(numbers)
}

example(5, 6, 3, 7, 8, 9)
  • Output: 5 6 3 and [7, 8, 9].
  • Important rule: the rest parameter must always be the last parameter in a function's parameter list.
    function bad(...numbers, x, y, z) { }  // SyntaxError: "Rest parameter must be the last formal parameter"

11. Callback Functions

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some routine or action.

Example

function greeting(name) {
  console.log(`Hello, ${name}`)
}

function processUser(callback) {
  const name = 'Roadside Coder'
  callback(name)
}

processUser(greeting) // "Hello, Roadside Coder"
  • greeting is the callback — passed into processUser and invoked ("called back") from within it.

Real-World Examples of Callbacks

  • Event listeners:
    document.addEventListener('click', function () {
      console.log('Clicked!')
    })
  • Built-in array methods: map, filter, reduce.
  • setTimeout.

12. Arrow Functions vs Regular Functions

Basic Syntax Conversion

// Regular function
function add(a, b) {
  return a + b
}

// Arrow function
const add = (a, b) => a + b
  • Arrow functions (introduced in ES6) offer a more concise syntax.
  • If the function body is a single expression, curly braces {} and the return keyword can both be omitted (implicit return).

Four Key Differences

# Feature Regular Function Arrow Function
1 Syntax function keyword => (arrow) syntax
2 Implicit return Requires explicit return (unless omitted intentionally) Can omit return for single-expression bodies
3 arguments object Available (an array-like object of all passed arguments) Not available — throws ReferenceError: arguments is not defined
4 this binding this is dynamically determined by how the function is called this is lexically inherited from the surrounding scope

arguments Keyword Example

function regular() {
  console.log(arguments)
}
regular(1, 2, 3) // Arguments(3) [1, 2, 3]

const arrow = () => {
  console.log(arguments)
}
arrow(1, 2, 3) // ReferenceError: arguments is not defined

this Keyword Example

const obj = {
  username: 'roadside coder',
  arrowFn: () => {
    console.log(`Subscribe to ${this.username}`)
  },
  regularFn: function () {
    console.log(`Subscribe to ${this.username}`)
  },
}

obj.arrowFn() // "Subscribe to undefined"
obj.regularFn() // "Subscribe to roadside coder"
  • In arrowFn, this does not refer to obj — it refers to the global object (or whatever this was in the enclosing lexical scope), since arrow functions don't have their own this binding.
  • In regularFn, this correctly refers to obj, since regular functions determine this based on how they are called (here, called as obj.regularFn()).

13. Closures — Introduction

Closures are the most important topic for JavaScript interviews — there can be hundreds of possible interview questions built around this single concept.

Informal Definition

A closure is a function that references variables in the outer scope from its inner scope.

  • Understanding closures requires first understanding lexical scope.

14. Lexical Scope

Lexical scope means a variable defined outside a function can be accessed inside another function defined after that variable's declaration — but not the other way around.

Example — Outer Variable Accessible Inside a Function

const username = 'roadside coder'

function local() {
  console.log(username) // "roadside coder" — accessible!
}

local()

Example — Inner Variable NOT Accessible Outside

function local() {
  const username = 'roadside coder'
}

console.log(username) // ReferenceError: username is not defined
  • Variables declared inside a function are not accessible from outside that function — lexical scoping only flows outward-to-inward, never the reverse.

Example — Nested Scopes and Closures

function subscribe() {
  const name = 'roadside coder'

  function displayName() {
    console.log(name) // accessible via lexical scope
  }

  displayName()
}

subscribe() // prints "roadside coder"
  • Here, displayName is a closure — it can access name from its outer function's scope (subscribe), even though name isn't defined inside displayName itself.

15. Closures — Formal Definition and Examples

MDN Definition

A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function.

  • Closures are created every time a function is created in JavaScript.

Classic MDN Example

function makeFunc() {
  const name = 'Mozilla'
  function displayName() {
    console.log(name)
  }
  return displayName
}

const myFunc = makeFunc()
myFunc() // "Mozilla"
  • makeFunc() returns the inner function displayName itself (not just its result).
  • Even after makeFunc has finished executing, the returned displayName function retains access to name via its closure — this is the defining feature of closures.
  • In JavaScript, every time a function is created, it "binds" to its environment/lexical scope — regardless of whether the function is called immediately or returned and called later.

Practical Use: Private Variables

Closures make it possible for a function to have private variables, controlling what is and isn't in scope, and which variables are shared between sibling functions within the same containing scope.

Alternative Calling Syntax

makeFunc()() // calling the returned function immediately

Passing Arguments Through Closures

function makeFunc() {
  const name = 'Mozilla'
  return function (num) {
    console.log(num)
  }
}

makeFunc()(5) // 5
  • The returned inner function behaves exactly like a normal function — it can accept its own parameters, while also having access to the outer scope's variables.

16. Closure Scope Chain

Every closure has (at least) three scopes:

  1. Its own local scope.
  2. The outer function's scope.
  3. The global scope.
  • Importantly, a function has access not just to its immediate outer scope, but to all enclosing scopes, all the way up to the global scope — this chain of accessible scopes is called the scope chain.

MDN Multi-Level Nesting Example

function a(x) {
  function b(y) {
    function c(z) {
      console.log(x + y + z)
    }
    return c
  }
  return b
}

a(1)(2)(3) // 6
  • Extended with a 4th level and a global variable e:
let e = 20

function a(x) {
  function b(y) {
    function c(z) {
      function d(w) {
        console.log(w + x + y + z + e)
      }
      return d
    }
    return c
  }
  return b
}

a(1)(2)(3)(4) // 20 (accesses w=4, x=1, y=2, z=3, e=20 → total 30, per the transcript's example logic)
  • The innermost function has access to every enclosing scope's variables — demonstrating the full scope chain in action.

17. Closure Interview Question: Block Scope & Shadowing

let count = 0

;(function printCount() {
  if (count === 0) {
    let count = 1
    console.log(count) // ?
  }
  console.log(count) // ?
})()

Answer

  • First console.log(count)1
  • Second console.log(count)0

Explanation

  • Inside the if block, let count = 1 creates a new, block-scoped variable that shadows the outer count — but only within that block.
  • Outside the if block (but still inside the IIFE), the outer count (value 0) remains unaffected by the shadowing inside the block.
  • This tests understanding of block scope and shadowing.

18. Closure Interview Question: Function Factory (Adder)

Goal

const addSix = createBase(6)
addSix(10) // 16
addSix(21) // 27

Implementation

function createBase(num) {
  return function (innerNum) {
    return innerNum + num
  }
}

const addSix = createBase(6)
console.log(addSix(10)) // 16
console.log(addSix(21)) // 27
  • createBase(6) creates a closure where num is permanently fixed at 6, accessible to the returned inner function.
  • This is a common use case for closures: preserving a value passed to an outer function, even after that outer function has finished executing and returned.

19. Closure Interview Question: Optimizing Code with Closures

Problem

function find(index) {
  const arr = []
  for (let i = 0; i < 1000000; i++) {
    arr[i] = i * i
  }
  console.log(arr[index])
}

console.time('6')
find(6)
console.timeEnd('6') // e.g., ~67 ms

console.time('50')
find(50)
console.timeEnd('50') // e.g., ~135 ms
  • Since the expensive loop (arr[i] = i * i, run 1,000,000 times) is identical every time find is called — regardless of index — this repeated computation is wasteful.

Optimized Version Using Closures

const closureFind = (function () {
  const arr = []
  for (let i = 0; i < 1000000; i++) {
    arr[i] = i * i
  }
  return function (index) {
    console.log(arr[index])
  }
})()

console.time('6')
closureFind(6)
console.timeEnd('6') // e.g., ~0.25 ms

console.time('50')
closureFind(50)
console.timeEnd('50') // e.g., ~0.025 ms
  • The expensive array-building loop runs only once (immediately, via an IIFE), and its result (arr) is captured in a closure.
  • Subsequent calls only perform the cheap arr[index] lookup — a massive performance improvement.
  • Key insight: closures allow expensive, unchanging computations to be cached/reused, rather than recomputed on every call.

20. Classic Interview Question: var, let, and setTimeout in Loops

The Question

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000)
}

Common (Incorrect) Guess

  • Many expect: 0, 1, 2 (printed one second apart).

Actual Output

  • 3, 3, 3 (all printed after ~1 second, since the delay is the same 1000 for all iterations in this example).

Why This Happens

  • var is function-scoped, not block-scoped — so there is only one shared i variable across all loop iterations.
  • setTimeout callbacks only execute after the entire synchronous loop has finished running.
  • By the time the loop completes, i has already reached its final value (3), and all three callbacks reference that same, final value of i — hence 3, 3, 3.

The Fix: Use let

for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000)
}
// Output: 0, 1, 2
  • let is block-scoped — each iteration of the loop creates a brand new binding of i, scoped just to that iteration.
  • Each setTimeout callback closes over its own separate copy of i, correctly capturing the value at the time of that specific iteration.

21. Fixing the var + Loop Problem Using Closures

The Challenge

Print 0, 1, 2 using var only (not let), using closures.

Solution

for (var i = 0; i < 3; i++) {
  ;(function inner(i) {
    setTimeout(function () {
      console.log(i)
    }, 1000)
  })(i)
}
// Output: 0, 1, 2
  • By wrapping the logic in an IIFE (inner) and passing i as a parameter, a new, separate copy of i (as a local parameter) is created for each iteration — mimicking what let's block scope does automatically.
  • Each setTimeout callback then closes over its own distinct local i, rather than sharing one single var i across all iterations.

22. Closure Interview Question: Private Counter

Goal

Create a counter whose internal value cannot be directly accessed or modified from outside — only via specific exposed methods.

Implementation

function counter() {
  let _counter = 0 // underscore prefix by convention, signals "private"

  function add(increment) {
    _counter += increment
  }

  function retrieve() {
    return `Counter = ${_counter}`
  }

  return { add, retrieve }
}

const c = counter()
c.add(5)
c.add(10)
console.log(c.retrieve()) // "Counter = 15"
  • _counter cannot be accessed directly from outside counter() — the only way to interact with it is through the returned add and retrieve functions, which close over _counter.
  • This is a foundational example of using closures to implement data encapsulation / private state in JavaScript.

23. Module Pattern

The module pattern exposes only selected ("public") functions while keeping other ("private") functions/variables completely inaccessible from outside the module.

Structure

const myModule = (function () {
  function privateMethod() {
    console.log('This is private')
  }

  function publicMethod() {
    console.log('This is public')
    privateMethod() // public methods CAN call private ones internally
  }

  return { publicMethod } // only publicMethod is exposed
})()

myModule.publicMethod() // works fine
myModule.privateMethod() // TypeError: myModule.privateMethod is not a function
  • Private functions are not returned, making them inaccessible outside the module's own scope — useful for internal helper logic (e.g., API calls) that shouldn't be exposed or manipulated directly by external code.
  • Public functions can call private functions internally, giving controlled access to otherwise hidden logic.
  • Frequently asked in senior developer interviews, though it can also appear in junior-level interviews.

24. Closure Interview Question: Run Function Only Once (once Polyfill)

Simple, Specific Version

function isSubscribed() {
  let called = 0

  return function (message) {
    if (called > 0) {
      console.log('Already subscribed to Roadside Coder')
    } else {
      console.log(message)
      called++
    }
  }
}

const subscribe = isSubscribed()
subscribe('Subscribe to Roadside Coder') // "Subscribe to Roadside Coder"
subscribe('Subscribe to Roadside Coder') // "Already subscribed to Roadside Coder"
  • called is captured by closure and persists across multiple calls to the returned inner function — enabling the "only run full logic once" behavior.

Generic, Reusable once Polyfill (Lodash-style)

function once(fn, context) {
  let ran // stores the result after the first call

  return function (...args) {
    if (fn) {
      ran = fn.apply(context || this, args)
      fn = null // prevent future re-invocation of the original function
    }
    return ran
  }
}

Usage

function hello() {
  console.log('hello')
}

const runOnce = once(hello)
runOnce() // "hello"
runOnce() // (nothing — fn is now null; returns cached "ran" value)

With Arguments

function sum(a, b) {
  console.log(a, b)
}

const onceSum = once(sum)
onceSum(1, 2) // 1 2 (only executes fully the first time)
  • fn.apply(context || this, args) — invokes the original function with the correct this context and array of arguments, returning its result.
  • Setting fn = null after the first call prevents any subsequent execution of the original logic, while still returning the cached result on future calls.

25. Closure Interview Question: Memoization Polyfill

Memoization = caching the result of an expensive function call so that repeated calls with the same arguments return instantly, without recomputation.

The Problem

function clumsyProduct(a, b) {
  // some expensive calculation...
  return a * b
}

console.time('first')
clumsyProduct(5, 6)
console.timeEnd('first') // e.g., ~40 ms

console.time('second')
clumsyProduct(5, 6)
console.timeEnd('second') // e.g., ~42 ms (recomputed unnecessarily)

myMemoize Implementation

function myMemoize(fn, context) {
  const res = {} // cache storage

  return function (...args) {
    const argsCache = JSON.stringify(args)

    if (!res[argsCache]) {
      res[argsCache] = fn.call(context || this, ...args)
    }

    return res[argsCache]
  }
}

Usage

const memoizedProduct = myMemoize(clumsyProduct)

console.time('first')
memoizedProduct(5, 6)
console.timeEnd('first') // ~49 ms (first call — computes and caches)

console.time('second')
memoizedProduct(5, 6)
console.timeEnd('second') // ~0.08 ms (cached result returned instantly)

Key Mechanics

  • JSON.stringify(args) converts the arguments array into a string key, used to check/store results in the res cache object.
  • If the result for that specific argument combination already exists in res, it is returned immediately — skipping the expensive computation.
  • Otherwise, the function is computed once, and the result is stored in the cache for future calls with the same arguments.
  • fn.call(context || this, ...args) invokes the original function with proper this binding and spread arguments.

26. Closure vs Scope

Concept Definition
Closure When a function is defined within another function, the inner function is a closure — it's usually returned so the outer function's variables remain accessible later.
Scope Defines which variables you have access to at a given point in code. There are two basic kinds: global scope and local scope.
  • In the specific context of closures, there are effectively three relevant scopes: global scope, outer (enclosing) function scope, and the closure's own local scope.

27. Currying — Introduction

Currying is a technique where a function takes one argument at a time and returns a new function expecting the next argument — converting a function callable as f(a, b, c) into one callable as f(a)(b)(c).

  • Curried functions are constructed by chaining closures, with each inner function immediately returned by its enclosing function.

Basic Example

// Normal function
function f(a, b) {
  console.log(a, b)
}

// Curried version
function f(a) {
  return function (b) {
    console.log(a, b)
  }
}

f(5)(6) // 5 6
  • Calling f(5) returns a new function still expecting b; calling that returned function with 6 finally executes the logic.
  • Currying can be extended to any number of levels/arguments, depending on how deeply nested the returned functions are.

Why Use Currying?

  • To avoid passing the same variable repeatedly.
  • To create higher-order functions.
  • To make functions more "pure" and less error-prone.

28. Currying Interview Question: Sum of N Arguments

Goal

sum(2)(6)(1) // 9

Step 1: Normal (Non-Curried) Version

function sum(a, b, c) {
  return a + b + c
}

sum(2, 6, 1) // 9

Step 2: Curried Version

function sum(a) {
  return function (b) {
    return function (c) {
      return a + b + c
    }
  }
}

console.log(sum(2)(6)(1)) // 9

Explanation for an Interview

  1. sum(2) returns a function still expecting b.
  2. Calling that with (6) returns another function still expecting c.
  3. Calling that final function with (1) computes and returns a + b + c = 9.

29. Currying Interview Question: evaluate Function

Goal

evaluate('sum')(4, 2) // 6
evaluate('multiply')(4, 2) // 8

Implementation

function evaluate(operation) {
  return function (a, b) {
    if (operation === 'sum') {
      return a + b
    } else if (operation === 'multiply') {
      return a * b
    } else if (operation === 'subtract') {
      return a - b
    } else if (operation === 'divide') {
      return a / b
    } else {
      return 'Invalid operation'
    }
  }
}

Usage

console.log(evaluate('sum')(4, 2)) // 6
console.log(evaluate('multiply')(4, 2)) // 8

Reusable, Pre-Configured Functions

const mul = evaluate('multiply')

console.log(mul(3, 5)) // 15
console.log(mul(2, 6)) // 12
  • Since evaluate("multiply") is only initialized once, the returned function mul can be reused repeatedly without re-specifying the operation each time — a practical benefit of currying.

30. Currying Interview Question: Infinite Currying

Goal

Support calling a function with any number of arguments, in any number of curried "chunks":

sum(1)(1)(2) // 3
sum(1)(1)(2)(4)(5) // 12

Solution

function add(a) {
  return function (b) {
    if (b) {
      return add(a + b)
    }
    return a
  }
}

console.log(add(5)(2)(4)(8)()) // 19

Step-by-Step Explanation

  1. add(5) returns a function still expecting the next value, with a = 5.
  2. Calling that with (2): since b = 2 is truthy, it recursively calls add(5 + 2) = add(7), returning yet another function.
  3. Calling with (4): add(7 + 4) = add(11).
  4. Calling with (8): add(11 + 8) = add(19).
  5. Finally, calling with no argument (()): since b is undefined (falsy), the function returns the accumulated value a = 19.
  • Key mechanism: the recursive call add(a + b) keeps returning a new function as long as another argument is supplied; calling the chain with an empty argument list signals "stop accumulating, return the final result."

31. Currying vs Partial Application

The Distinction

Currying: the number of nested functions a curried function has must equal the number of arguments it ultimately receives. Partial application: transforms a function into another function with smaller arity (fewer expected arguments per call), without necessarily matching argument count to nesting depth.

Currying Example (3 arguments → 3 nested functions)

function sum(a) {
  return function (b) {
    return function (c) {
      return a + b + c
    }
  }
}

sum(10)(20)(30) // fully curried

Partial Application Example (3 arguments → only 2 nested functions)

function sum(a, b) {
  return function (c) {
    return a + b + c
  }
}

sum(10, 20)(30) // partial application, NOT full currying
  • This is not currying, because there are 3 arguments but only 2 returned/nested functions — it's a partial application of the sum function.
  • Arity = the number of arguments/operands a function expects.

32. Real-World Use Case: Currying for DOM Manipulation

Scenario

<h1 id="heading">Hello Piyush</h1>

Implementation

function updateElementText(id) {
  return function (content) {
    document.querySelector(`#${id}`).textContent = content
  }
}

const updateHeader = updateElementText('heading')

updateHeader('Hello, Roadside Coder') // updates the <h1> text
updateHeader('Welcome, Roadside Coder') // updates it again
  • updateElementText("heading") is initialized once, permanently "remembering" the target element's ID via closure.
  • The returned updateHeader function can then be called repeatedly (e.g., on button clicks or other events) to update the same element's text — without needing to re-query the DOM by ID each time.
  • This demonstrates a practical, real-world application of currying beyond pure academic exercises — a strong answer to impress interviewers.

33. Building a Generic curry Polyfill

Goal

Write a function curry(fn) that converts any normal function into its fully curried equivalent — automatically, regardless of how many parameters fn has.

Implementation

function curry(fn) {
  return function curried(...args) {
    if (fn.length <= args.length) {
      return fn(...args)
    } else {
      return function (...nextArgs) {
        return curried(...args, ...nextArgs)
      }
    }
  }
}

Usage

function sum(a, b, c, d) {
  return a + b + c + d
}

const totalSum = curry(sum)

console.log(totalSum(1)(2)(3)(4)) // 10 (fully curried, one arg at a time)
console.log(totalSum(1, 2)(3)(4)) // 10 (also works with multiple args per call)
console.log(totalSum(1, 2, 3, 4)) // 10 (also works as a normal call)

Step-by-Step Explanation

  1. curry(fn) returns an inner function curried, which collects arguments into an args array (via the rest operator).
  2. On each call, it checks: is fn.length (the number of parameters fn expects) ≤ the number of arguments collected so far (args.length)?
    • If yes: enough arguments have been gathered — call the original function fn with all collected args, and return the result.
    • If no: return another function that, when called with more arguments (nextArgs), recursively calls curried again with all previous args plus the new ones combined (via spread).
  3. This recursive "keep collecting until we have enough" pattern is what makes the polyfill flexible — it works whether the caller passes one argument at a time, multiple at once, or all of them together.

Why This Matters

  • This is considered one of the most important and frequently asked currying questions in senior-level JavaScript interviews (though it can occasionally appear in junior interviews too).
  • Demonstrates a deep, practical understanding of closures, recursion, rest/spread operators, and function arity (Function.prototype.length) all combined into one elegant utility.

34. Key Terms Glossary

Term Definition
Function declaration A named function defined with the function keyword (also called function statement/definition)
Function expression A function (often anonymous) stored in a variable
Anonymous function A function with no name, often used in expressions or as callbacks
First-class function A function that can be treated like any other value — passed around, returned, assigned to variables
IIFE Immediately Invoked Function Expression — a function that executes as soon as it's defined
Hoisting JavaScript's behavior of processing declarations before executing code; functions are hoisted with their full body, variables (var) only with their declaration (as undefined)
Shadowing When a variable declared in an inner scope has the same name as one in an outer scope, temporarily "hiding" the outer variable within that inner scope
Params (parameters) The named variables listed in a function's definition
Arguments The actual values passed to a function when it is called
Spread operator (...) Expands an iterable (e.g., array) into individual elements/arguments
Rest operator (...) Collects multiple arguments into a single array
Callback function A function passed as an argument to another function, to be invoked later
Arrow function ES6 concise function syntax; lacks its own this and arguments binding
this A reference that depends on how a function is called (regular functions) or is lexically inherited (arrow functions)
Closure A function bundled with references to its surrounding (lexical) state, giving it access to an outer function's scope even after that outer function has returned
Lexical scope The scope determined by where variables/functions are defined in the source code (not where they are called from)
Scope chain The chain of nested scopes (local → outer → global) that a function can access variables from
Block scope Scope limited to a { } block, applicable to let/const (not var)
Private variable A variable made inaccessible from outside a function/module, often implemented via closures
Module pattern A design pattern using closures to expose only selected "public" functions while hiding "private" ones
Memoization Caching a function's results based on its input arguments, to avoid redundant expensive computation
Polyfill Custom code that replicates the behavior of an existing (often built-in) function or feature
Currying Transforming a function so it takes one argument at a time, returning a new function for each subsequent argument
Partial application Transforming a function into one with fewer expected arguments per call, without requiring nesting to match total argument count exactly
Arity The number of arguments/operands a function expects
Function.prototype.length A built-in property returning the number of declared parameters a function expects (used in curry polyfills)
JSON.stringify Converts a JavaScript value (e.g., an arguments array) into a string — used here to create cache keys for memoization
Function.prototype.apply / .call Methods to invoke a function with an explicit this context and set of arguments

Summary

This course covered three deeply interconnected JavaScript interview topics:

  1. Functions fundamentals — declarations vs expressions, first-class functions, IIFEs, scope, hoisting (and the subtle differences between function and variable hoisting), params vs arguments, spread/rest operators, callbacks, and arrow functions vs regular functions (especially around this and arguments).

  2. Closures — built on top of lexical scope, closures allow inner functions to retain access to their outer function's variables even after the outer function has returned. This underpins many practical patterns: performance optimization (caching expensive computations), the classic var/let + setTimeout loop problem, private counters, the module pattern, once and memoization polyfills — all frequently asked in interviews at every seniority level.

  3. Currying — converting functions from f(a, b, c) style into f(a)(b)(c) style by chaining closures. Key interview questions include implementing curried sum/evaluate functions, infinite currying, distinguishing currying from partial application, real-world use cases (like DOM manipulation), and — most importantly — writing a generic curry polyfill that can convert any function into its curried form automatically.

Across all three topics, the recurring theme is that JavaScript's function-first design (first-class functions + closures) is what enables powerful, flexible patterns like caching, private state, and currying — concepts that interviewers frequently probe to assess a candidate's depth of understanding beyond basic syntax.