Source: Roadside Coder — JavaScript Interview Questions Course Topics: Function declarations/expressions, scope, hoisting, closures, lexical scope, currying, partial application, polyfills
- Function Declaration vs Function Expression
- First-Class Functions
- IIFE — Immediately Invoked Function Expression
- Function Scope
varvsletin Loops (Output-Based Question)- Hoisting
- Function Hoisting vs Variable Hoisting
- Tricky Hoisting Output Question
- Params vs Arguments
- Spread vs Rest Operators
- Callback Functions
- Arrow Functions vs Regular Functions
- Closures — Introduction
- Lexical Scope
- Closures — Formal Definition and Examples
- Closure Scope Chain
- Closure Interview Question: Block Scope & Shadowing
- Closure Interview Question: Function Factory (Adder)
- Closure Interview Question: Optimizing Code with Closures
- Classic Interview Question:
var,let, andsetTimeoutin Loops - Fixing the
var+ Loop Problem Using Closures - Closure Interview Question: Private Counter
- Module Pattern
- Closure Interview Question: Run Function Only Once (
oncePolyfill) - Closure Interview Question: Memoization Polyfill
- Closure vs Scope
- Currying — Introduction
- Currying Interview Question: Sum of N Arguments
- Currying Interview Question:
evaluateFunction - Currying Interview Question: Infinite Currying
- Currying vs Partial Application
- Real-World Use Case: Currying for DOM Manipulation
- Building a Generic
curryPolyfill - Key Terms Glossary
function square(number) {
return number * number
}- Also called a function definition or function statement.
- Declared using the
functionkeyword followed directly by a name.
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).
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.
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."
function square(number) {
return number * number
}
function displaySquare(fn, number) {
console.log(`square is ${fn(number)}`)
}
displaySquare(square, 5) // "square is 25"- Here,
squareis passed intodisplaySquarejust like a variable would be — demonstrating first-class function behavior.
IIFE = Immediately Invoked Function Expression — a function that runs as soon as it is defined, without needing a separate call.
;(function (number) {
console.log(number * number)
})(5)- Wrap the function in parentheses, then immediately invoke it with another set of parentheses (optionally passing arguments).
;(function () {
var x = 1
;(function () {
var y = 2
console.log(x)
})()
})()- Question: What does this print?
- Common wrong guess:
undefined(sincexisn't defined in the inner scope). - Correct answer:
1. - Why: JavaScript searches for
xin the inner scope first; not finding it, it searches the parent scope, wherex = 1exists. This behavior is due to closures (explained in detail later in this document).
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
num1andnum2are not defined insidemultiply, the function looks them up in the global scope.
function getScore() {
const num1 = 2
const num2 = 3
return `${name} scored ${num1 + num2}`
}
console.log(getScore()) // "roadside coder scored 5"- Even though
num1andnum2also exist in the global scope, the localnum1/num2inside the function shadow (override) the global versions within that function's scope. nameis still taken from the global scope, since it isn't redefined locally.
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 1000)
}- Output:
5, 5, 5, 5, 5— becausevaris function-scoped, not block-scoped. By the time eachsetTimeoutcallback runs, the loop has already finished, andiholds its final value (5).
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 1000)
}- Output:
0, 1, 2, 3, 4— becauseletis block-scoped, meaning each iteration of the loop creates a new block scope with its own separate copy ofi.
This distinction is explained fully in Section 20.
- 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:
- Initialization phase — the entire scope's variables and functions are scanned and set up first.
- Execution phase — the code actually runs, line by line.
console.log(x) // undefined (not an error!)
var x = 5
console.log(x) // 5var xis hoisted, but only the declaration, not the assignment — so accessing it before the assignment line givesundefined, not aReferenceError.
sayHello() // works fine, even though called before declaration
function sayHello() {
console.log('Hello!')
}- Functions declared with the
functionkeyword 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
undefinedif accessed early.
- Inspecting the browser's Sources panel with a breakpoint at the top of a script shows:
- Variables (e.g.,
x) hoisted asundefined. - Functions (e.g.,
sayHello) hoisted with their complete function definition already available.
- Variables (e.g.,
- This same hoisting behavior (complete initialization phase, then execution phase) also applies within function scopes, not just the global scope.
var x = 21
;(function () {
console.log(x)
var x = 20
})()What does console.log(x) print inside the IIFE — 21 (global) or 20 (local)?
- Hoisting is a two-step process:
- First, the global scope is initialized (
x = 21). - Then, when the IIFE runs, its own local scope is initialized — which hoists its own local
var xdeclaration to the top of that function, setting it toundefinedbefore the assignment (var x = 20) executes.
- First, the global scope is initialized (
- 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 (currentlyundefined) localx. - 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.
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.
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.
function multiply(...nums) {
console.log(nums) // [5, 6]
return nums[0] * nums[1]
}- Here,
...numscollects all passed arguments into a single array — called the rest operator.
function example(x, y, z, ...numbers) {
console.log(x, y, z)
console.log(numbers)
}
example(5, 6, 3, 7, 8, 9)- Output:
5 6 3and[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"
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.
function greeting(name) {
console.log(`Hello, ${name}`)
}
function processUser(callback) {
const name = 'Roadside Coder'
callback(name)
}
processUser(greeting) // "Hello, Roadside Coder"greetingis the callback — passed intoprocessUserand invoked ("called back") from within it.
- Event listeners:
document.addEventListener('click', function () { console.log('Clicked!') })
- Built-in array methods:
map,filter,reduce. setTimeout.
// 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 thereturnkeyword can both be omitted (implicit return).
| # | 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 |
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 definedconst 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,thisdoes not refer toobj— it refers to the global object (or whateverthiswas in the enclosing lexical scope), since arrow functions don't have their ownthisbinding. - In
regularFn,thiscorrectly refers toobj, since regular functions determinethisbased on how they are called (here, called asobj.regularFn()).
Closures are the most important topic for JavaScript interviews — there can be hundreds of possible interview questions built around this single concept.
A closure is a function that references variables in the outer scope from its inner scope.
- Understanding closures requires first understanding 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.
const username = 'roadside coder'
function local() {
console.log(username) // "roadside coder" — accessible!
}
local()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.
function subscribe() {
const name = 'roadside coder'
function displayName() {
console.log(name) // accessible via lexical scope
}
displayName()
}
subscribe() // prints "roadside coder"- Here,
displayNameis a closure — it can accessnamefrom its outer function's scope (subscribe), even thoughnameisn't defined insidedisplayNameitself.
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.
function makeFunc() {
const name = 'Mozilla'
function displayName() {
console.log(name)
}
return displayName
}
const myFunc = makeFunc()
myFunc() // "Mozilla"makeFunc()returns the inner functiondisplayNameitself (not just its result).- Even after
makeFunchas finished executing, the returneddisplayNamefunction retains access tonamevia 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.
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.
makeFunc()() // calling the returned function immediatelyfunction 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.
Every closure has (at least) three scopes:
- Its own local scope.
- The outer function's scope.
- 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.
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.
let count = 0
;(function printCount() {
if (count === 0) {
let count = 1
console.log(count) // ?
}
console.log(count) // ?
})()- First
console.log(count)→1 - Second
console.log(count)→0
- Inside the
ifblock,let count = 1creates a new, block-scoped variable that shadows the outercount— but only within that block. - Outside the
ifblock (but still inside the IIFE), the outercount(value0) remains unaffected by the shadowing inside the block. - This tests understanding of block scope and shadowing.
const addSix = createBase(6)
addSix(10) // 16
addSix(21) // 27function createBase(num) {
return function (innerNum) {
return innerNum + num
}
}
const addSix = createBase(6)
console.log(addSix(10)) // 16
console.log(addSix(21)) // 27createBase(6)creates a closure wherenumis permanently fixed at6, 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.
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 timefindis called — regardless ofindex— this repeated computation is wasteful.
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.
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}- Many expect:
0, 1, 2(printed one second apart).
3, 3, 3(all printed after ~1 second, since the delay is the same1000for all iterations in this example).
varis function-scoped, not block-scoped — so there is only one sharedivariable across all loop iterations.setTimeoutcallbacks only execute after the entire synchronous loop has finished running.- By the time the loop completes,
ihas already reached its final value (3), and all three callbacks reference that same, final value ofi— hence3, 3, 3.
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
// Output: 0, 1, 2letis block-scoped — each iteration of the loop creates a brand new binding ofi, scoped just to that iteration.- Each
setTimeoutcallback closes over its own separate copy ofi, correctly capturing the value at the time of that specific iteration.
0, 1, 2usingvaronly (notlet), using closures.
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 passingias a parameter, a new, separate copy ofi(as a local parameter) is created for each iteration — mimicking whatlet's block scope does automatically. - Each
setTimeoutcallback then closes over its own distinct locali, rather than sharing one singlevar iacross all iterations.
Create a counter whose internal value cannot be directly accessed or modified from outside — only via specific exposed methods.
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"_countercannot be accessed directly from outsidecounter()— the only way to interact with it is through the returnedaddandretrievefunctions, which close over_counter.- This is a foundational example of using closures to implement data encapsulation / private state in JavaScript.
The module pattern exposes only selected ("public") functions while keeping other ("private") functions/variables completely inaccessible from outside the module.
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.
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"calledis captured by closure and persists across multiple calls to the returned inner function — enabling the "only run full logic once" behavior.
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
}
}function hello() {
console.log('hello')
}
const runOnce = once(hello)
runOnce() // "hello"
runOnce() // (nothing — fn is now null; returns cached "ran" value)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 correctthiscontext and array of arguments, returning its result.- Setting
fn = nullafter the first call prevents any subsequent execution of the original logic, while still returning the cached result on future calls.
Memoization = caching the result of an expensive function call so that repeated calls with the same arguments return instantly, without recomputation.
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)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]
}
}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)JSON.stringify(args)converts the arguments array into a string key, used to check/store results in therescache 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 properthisbinding and spread arguments.
| 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.
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 asf(a)(b)(c).
- Curried functions are constructed by chaining closures, with each inner function immediately returned by its enclosing function.
// 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 expectingb; calling that returned function with6finally executes the logic. - Currying can be extended to any number of levels/arguments, depending on how deeply nested the returned functions are.
- To avoid passing the same variable repeatedly.
- To create higher-order functions.
- To make functions more "pure" and less error-prone.
sum(2)(6)(1) // 9function sum(a, b, c) {
return a + b + c
}
sum(2, 6, 1) // 9function sum(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
console.log(sum(2)(6)(1)) // 9sum(2)returns a function still expectingb.- Calling that with
(6)returns another function still expectingc. - Calling that final function with
(1)computes and returnsa + b + c=9.
evaluate('sum')(4, 2) // 6
evaluate('multiply')(4, 2) // 8function 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'
}
}
}console.log(evaluate('sum')(4, 2)) // 6
console.log(evaluate('multiply')(4, 2)) // 8const mul = evaluate('multiply')
console.log(mul(3, 5)) // 15
console.log(mul(2, 6)) // 12- Since
evaluate("multiply")is only initialized once, the returned functionmulcan be reused repeatedly without re-specifying the operation each time — a practical benefit of currying.
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) // 12function add(a) {
return function (b) {
if (b) {
return add(a + b)
}
return a
}
}
console.log(add(5)(2)(4)(8)()) // 19add(5)returns a function still expecting the next value, witha = 5.- Calling that with
(2): sinceb = 2is truthy, it recursively callsadd(5 + 2)=add(7), returning yet another function. - Calling with
(4):add(7 + 4)=add(11). - Calling with
(8):add(11 + 8)=add(19). - Finally, calling with no argument (
()): sincebisundefined(falsy), the function returns the accumulated valuea=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."
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.
function sum(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
sum(10)(20)(30) // fully curriedfunction 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
sumfunction. - Arity = the number of arguments/operands a function expects.
<h1 id="heading">Hello Piyush</h1>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 againupdateElementText("heading")is initialized once, permanently "remembering" the target element's ID via closure.- The returned
updateHeaderfunction 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.
Write a function curry(fn) that converts any normal function into its fully curried equivalent — automatically, regardless of how many parameters fn has.
function curry(fn) {
return function curried(...args) {
if (fn.length <= args.length) {
return fn(...args)
} else {
return function (...nextArgs) {
return curried(...args, ...nextArgs)
}
}
}
}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)curry(fn)returns an inner functioncurried, which collects arguments into anargsarray (via the rest operator).- On each call, it checks: is
fn.length(the number of parametersfnexpects) ≤ the number of arguments collected so far (args.length)?- If yes: enough arguments have been gathered — call the original function
fnwith all collectedargs, and return the result. - If no: return another function that, when called with more arguments (
nextArgs), recursively callscurriedagain with all previous args plus the new ones combined (via spread).
- If yes: enough arguments have been gathered — call the original function
- 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.
- 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.
| 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 |
This course covered three deeply interconnected JavaScript interview topics:
-
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
thisandarguments). -
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+setTimeoutloop problem, private counters, the module pattern,onceand memoization polyfills — all frequently asked in interviews at every seniority level. -
Currying — converting functions from
f(a, b, c)style intof(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 genericcurrypolyfill 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.