Function Types & Parameters Interview Questions & Answers
30 questions Updated 2026-06-18
JavaScript function types and parameters interview questions — declarations vs expressions vs arrows, IIFEs, default and rest parameters, the arguments object, destructuring, arity, and first-class functions.
Read the in-depth guideJavaScript Function Types & Parameters — Declarations, Arrows, Defaults and RestA function declaration stands alone as a statement and is hoisted whole, so you can call it before its line. A function expression assigns a function to a variable; only the variable is hoisted, not its value.
sayHi() // works — declaration hoisted
function sayHi() {}
sayBye() // TypeError: sayBye is not a function
var sayBye = function () {} // expression — value assigned at runtime
Pitfall: with let/const the expression variable is in the temporal
dead zone, so calling early throws a ReferenceError instead. Choose
declarations for top-level reusable functions and expressions when passing a
function as a value.
A named function has its own identifier; an anonymous function has none. The name aids recursion and stack traces.
const fac = function factorial(n) { // named function expression
return n <= 1 ? 1 : n * factorial(n - 1) // can call itself by name
}
[1].map(function () {}) // anonymous — appears unnamed in traces
Modern engines infer a name for anonymous functions assigned to a variable, so
const f = () => {} has f.name === 'f'. Pitfall: a named function
expression's inner name is only visible inside the function, not outside.
Arrow functions are more than shorter syntax. They have no own this,
arguments, super, or new.target — they inherit this lexically — and
they cannot be used as constructors.
const obj = {
items: [1, 2],
log() {
this.items.forEach(i => console.log(this.items)) // `this` is obj
}
}
const Arrow = () => {}
new Arrow() // TypeError: Arrow is not a constructor
Pitfall: don't use an arrow as an object method if you need this to
be the object — it will be the surrounding scope's this (often the module or
window), not the object.
An IIFE (Immediately Invoked Function Expression) is a function defined and called at once. It creates a private scope so its variables don't leak.
(function () {
const secret = 42 // not visible outside
console.log(secret)
})() // runs immediately
// arrow form
(() => { /* ... */ })()
The wrapping parens turn the declaration into an expression so it can be
invoked. IIFEs powered the old module pattern before ES modules. Pitfall:
a missing semicolon before an IIFE can make the previous line try to call it —
defensively prefix with ;.
First-class means functions are treated like any other value: they can be assigned to variables, stored in arrays/objects, passed as arguments, and returned from functions.
const ops = { add: (a, b) => a + b } // stored in an object
const fns = [Math.abs, Math.sqrt] // stored in an array
const run = fn => fn(16) // passed as argument
run(fns[1]) // 4
This is the foundation that makes higher-order functions, callbacks, and closures possible. Without first-class functions, none of the functional patterns in JS would work.
A default parameter supplies a value when the argument is undefined
(either omitted or explicitly undefined). The default expression is
evaluated at call time, only when needed.
function greet(name = 'friend') { return `Hi, ${name}` }
greet() // 'Hi, friend'
greet(undefined) // 'Hi, friend' — default applies
greet(null) // 'Hi, null' null is NOT undefined
Pitfall: only undefined triggers the default — passing null, 0, or
'' does not. Defaults can also reference earlier parameters:
(a, b = a * 2) => ....
Defaults are evaluated left to right at call time, each in a scope where earlier parameters are already bound but later ones are not. They are fresh every call, not cached.
let calls = 0
const next = () => ++calls
function f(a = next(), b = a + 1) { return [a, b] }
f() // [1, 2] — next() ran once, b saw a
f(10) // [10, 11] — default for a skipped, next() not called
Pitfall: referencing a later parameter in an earlier default throws a
ReferenceError (TDZ): function g(a = b, b = 1) {} — g(). Also, an object
default like { items: [] } creates a new array each call, avoiding the
shared-mutable-default trap seen in some other languages.
A rest parameter (...args) collects all remaining arguments into a
real array. It must be the last parameter and there can be only one.
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0) // nums is a true array
}
sum(1, 2, 3) // 6
function tagged(first, ...rest) { /* first separate, rest gathers the tail */ }
Unlike the old arguments object, rest params are a genuine Array (with
map, filter, etc.). Pitfall: rest params are excluded from
fn.length (arity).
arguments is an array-like object available inside regular (non-arrow)
functions, holding all passed arguments regardless of declared parameters.
function f() {
return arguments.length // works even with no named params
}
f(1, 2, 3) // 3
// it's array-LIKE, not an array
Array.prototype.slice.call(arguments) // convert to real array
[...arguments] // modern conversion
Pitfall: it lacks array methods (map, forEach), so convert it first.
In modern code, prefer rest parameters over arguments.
Arrow functions deliberately have no own arguments — like this, they
inherit it lexically from the enclosing regular function (or it's
undefined at module top level).
function outer() {
const inner = () => arguments[0] // refers to outer's arguments
return inner()
}
outer('hi') // 'hi'
const f = () => arguments // ReferenceError at top level
The fix when you need all args in an arrow is rest parameters:
const f = (...args) => args. This lexical behavior is exactly why arrows are
great as callbacks but unsuitable as methods needing their own arguments.
fn.length is the function's arity — the number of parameters before
the first one with a default value or a rest parameter. It does not count
those.
((a, b) => {}).length // 2
((a, b = 1) => {}).length // 1 — stops at first default
((a, ...rest) => {}).length // 1 — rest excluded
((a, b, c = 1, d) => {}).length // 2 — counts up to first default
Pitfall: because defaults and rest reduce length, generic helpers like
curry that rely on fn.length misbehave on such functions. length is
read-only.
fn.name is the function's name string, used in stack traces and
debugging. Engines infer it from the variable or property a function is
assigned to when the function itself is anonymous.
function foo() {}
foo.name // 'foo'
const bar = () => {}
bar.name // 'bar' — inferred
const o = { baz() {} }
o.baz.name // 'baz'
[].map(() => {}).name // '' or anonymous — not assigned to a binding
bind prepends 'bound ': foo.bind(null).name === 'bound foo'. Pitfall:
minifiers rename functions, so don't rely on name for program logic.
You can destructure an object or array argument right in the parameter list, pulling out the fields you need and optionally giving them defaults.
function createUser({ name, role = 'user', age } = {}) {
return `${name} (${role})`
}
createUser({ name: 'Ada', role: 'admin' }) // 'Ada (admin)'
createUser() // 'undefined (user)' no crash
The trailing = {} is crucial: it lets the function be called with no
argument without throwing. Pitfall: omitting it and calling
createUser() throws "Cannot destructure property of undefined".
A method is simply a function stored as an object property and usually
called with a receiver (obj.method()), so its this is that object. A plain
function is called standalone and its this depends on call context.
const calc = {
value: 10,
double() { return this.value * 2 } // method — `this` is calc
}
calc.double() // 20
const d = calc.double
d() // `this` is undefined — now a detached function call
Pitfall: detaching a method (const d = calc.double) loses the receiver;
this is determined by how it's called, not where it lives.
Method shorthand (foo() {} inside an object) is mostly equivalent to
foo: function () {}, but shorthand methods can use super and are
created as non-constructable (you can't new them).
const obj = {
greet() { return 'hi' }, // shorthand
greet2: function () { return 'hi' } // property with function
}
new obj.greet2() // works (legacy)
new obj.greet() // TypeError: not a constructor
Pitfall: the inability to construct shorthand methods is by design; it's rarely an issue but surprising if you relied on it.
Recursion is a function calling itself to solve a smaller version of a problem. Every recursive function needs a base case that stops the recursion and a recursive case that moves toward it.
function factorial(n) {
if (n <= 1) return 1 // base case
return n * factorial(n - 1) // recursive case, n shrinks
}
factorial(5) // 120
Pitfall: a missing or unreachable base case causes infinite recursion and a "Maximum call stack size exceeded" error. Deep recursion can also blow the stack even when correct.
A tail call is when a function's last action is to return the result of another call, with nothing left to do afterward. Proper Tail Calls (PTC) would let the engine reuse the stack frame, enabling unbounded recursion.
function fact(n, acc = 1) {
if (n <= 1) return acc
return fact(n - 1, n * acc) // tail position — no pending multiply
}
Reality: although PTC is in the ES2015 spec, only Safari/JavaScriptCore implements it; V8 (Chrome/Node) and Firefox do not. Pitfall: so don't rely on tail-call elimination for deep recursion — use a loop or an explicit stack instead.
Arity is the number of arguments a function expects — its declared
parameter count, reflected (with caveats) by fn.length.
const unary = x => x // arity 1
const binary = (a, b) => a+b // arity 2
binary.length // 2
Some HOFs depend on arity — e.g. a curry helper invokes once enough args
arrive. Pitfall: "expected" arity (length) and "actual" args passed can
differ freely in JS, since extra args are ignored and missing ones become
undefined.
A variadic function accepts a variable number of arguments. In modern
JS you express this with a rest parameter; older code read arguments.
const max = (...nums) => nums.reduce((m, n) => n > m ? n : m, -Infinity)
max(3, 9, 2) // 9
max(...[5, 1, 8]) // 8 — spread an array in
Math.max(1, 2, 3) // built-in variadic example
Pitfall: spreading a very large array into a variadic call can exceed
the engine's argument limit and throw — use reduce over the array directly
for huge inputs.
JavaScript is pass-by-value for everything — but for objects the "value" is a reference (a copy of the pointer). So you can mutate an object's contents, but reassigning the parameter doesn't affect the caller.
function mutate(o) { o.x = 1 } // caller sees x === 1
function reassign(o) { o = { x: 9 } } // caller unaffected
const a = {}; mutate(a) // a.x === 1
const b = {}; reassign(b) // b unchanged
Pitfall: this trips people who expect "pass by reference" — only the reference is copied, so reassigning the local parameter is invisible to the caller.
JavaScript is lenient about arity. Missing arguments are undefined; extra
arguments are ignored (but still available via arguments/rest).
function f(a, b) { return [a, b] }
f(1) // [1, undefined] — missing -> undefined
f(1, 2, 3) // [1, 2] — extra 3 ignored by params
This flexibility powers default parameters and variadic patterns. Pitfall:
because no error is thrown, a typo dropping an argument fails silently with
undefined downstream rather than at the call site.
The Function constructor builds a function from strings at runtime:
new Function('a', 'b', 'return a + b'). Like eval, it executes dynamic
code.
const add = new Function('a', 'b', 'return a + b')
add(2, 3) // 5
// does NOT close over local scope
function outer() {
const x = 1
return new Function('return x') // ReferenceError when called
}
Avoid it because: it bypasses lexical scope (only sees global), is a security/CSP risk, and can't be optimized. Use real functions or closures instead.
A generator function is declared with function* and can pause at
yield. Calling it doesn't run the body — it returns an iterator object.
function* gen() { yield 1; yield 2 }
const it = gen() // nothing logged yet
it.next() // { value: 1, done: false }
It's a distinct function type from declarations, expressions, and arrows.
Pitfall: there is no arrow generator syntax — *() => {} is invalid;
generators must use function*.
An async function always returns a Promise and may use await
inside to pause until a Promise settles. The async keyword changes the
function's return contract.
async function load() {
const r = await fetch('/api') // pauses without blocking the thread
return r.json() // wrapped in a Promise automatically
}
load().then(data => /* ... */ data) // returns a Promise
Arrows can be async too: const f = async () => {}. Pitfall: even a
return 5 inside an async function yields Promise<5>, not 5, so callers
must await or .then it.
A constructor function is a regular function intended to be called with
new. new creates a fresh object, sets its prototype, binds this to
it, runs the body, and returns the object.
function User(name) { this.name = name } // convention: capitalized
const u = new User('Ada') // this -> new object
User('Bob') // no `new`: `this` is undefined/global
Pitfall: forgetting new is a classic bug — this leaks to the global
object (or throws in strict mode). ES6 classes make this safer by throwing
if you call them without new.
Function declarations are fully hoisted — name and body — so they're
callable from the top of the scope. let/const expressions are hoisted
only as uninitialized bindings in the temporal dead zone (TDZ).
decl() // works
function decl() {}
expr() // ReferenceError — TDZ
const expr = () => {}
With var, the variable hoists as undefined, so calling early gives a
TypeError (undefined is not a function) instead. Pitfall: relying on
declaration hoisting can obscure code order — many style guides discourage it.
Yes — JS lets you mix all parameter features, though rest must come last and a rest parameter cannot have a default.
function config({ debug = false } = {}, ...plugins) {
return { debug, count: plugins.length }
}
config({ debug: true }, 'a', 'b') // { debug: true, count: 2 }
function bad(...args = []) {} // SyntaxError — rest can't default
Pitfall: ordering matters — a default-valued parameter after a rest
parameter is a syntax error, and forgetting the = {} on the destructured
object breaks no-argument calls.
Parameters are local variables of a function, so an inner function closes over them just like any other local. This is the basis of factory functions and partial application.
function multiplier(factor) { // `factor` is a parameter...
return n => n * factor // ...captured by the returned closure
}
const triple = multiplier(3)
triple(5) // 15 factor remembered
Each call to multiplier creates a new factor binding, so double and
triple don't interfere. Pitfall: capturing a parameter that's later
reassigned inside the outer function captures the latest value, not a
snapshot.
Getters/setters are functions defined with get/set that are invoked by
property access syntax rather than a call — obj.x runs the getter,
obj.x = 1 runs the setter.
const temp = {
_c: 0,
get fahrenheit() { return this._c * 9/5 + 32 },
set fahrenheit(f) { this._c = (f - 32) * 5/9 }
}
temp.fahrenheit = 212 // calls setter
temp.fahrenheit // 212 — calls getter, no parentheses
They enable computed/validated properties with a plain-property API. Pitfall: a getter that does heavy work runs on every access, and a getter without a matching setter makes the property silently read-only (or throws in strict mode on assignment).
The name of a named function expression is bound only inside the function's own body, not in the surrounding scope. It exists so the function can refer to itself.
const f = function rec(n) {
return n <= 0 ? 0 : n + rec(n - 1) // `rec` visible here
}
f(3) // 6
rec(3) // ReferenceError — `rec` not visible outside
This makes recursion safe against reassignment of the outer variable
(f). Pitfall: people expect the inner name to be accessible globally and
are surprised by the ReferenceError outside.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.