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 Rest

A 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.