JavaScript · Functions

JavaScript Function Types & Parameters — Declarations, Arrows, Defaults and Rest

7 min read Updated 2026-06-18

Practice Function Types & Parameters interview questions

So many ways to write a function

JavaScript offers several syntaxes for defining functions, each with different hoisting, this behavior, and use cases. It also has a flexible parameter system — defaults, rest parameters, destructuring, and the legacy arguments object. Knowing the distinctions lets you pick the right tool and avoid surprises around binding and hoisting. This guide maps out the function types and the parameter features that go with them.

Declarations vs expressions

A function declaration is a standalone statement; a function expression assigns a function to a variable. The crucial difference is hoisting.

sayHi()                          // works — declarations are fully hoisted
function sayHi() { return 'hi' }

sayBye()                         // TypeError — expression not yet assigned
const sayBye = function () { return 'bye' }

Declarations are hoisted entirely, so you can call them before their definition in the source. Expressions are not — the variable is hoisted but holds undefined until the assignment runs. Use declarations for top-level named functions; expressions when assigning conditionally or passing inline.

Named vs anonymous expressions

A function expression can be anonymous or carry a name. A named function expression helps in stack traces and allows the function to reference itself for recursion.

const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1)   // can call itself by its internal name
}
factorial(5)   // 120
// fact is NOT visible outside the function body

The internal name (fact) is scoped to the function itself, keeping the outer namespace clean while enabling self-reference.

Arrow functions

Arrow functions are concise and, crucially, don't have their own this, arguments, super, or new.target — they inherit this lexically from the surrounding scope. This makes them perfect for callbacks but unsuitable as methods or constructors.

const add = (a, b) => a + b          // implicit return
const square = n => n * n            // single param, no parens needed
const makeObj = () => ({ x: 1 })     // wrap object literal in parens

class Timer {
  seconds = 0
  start() {
    setInterval(() => { this.seconds++ }, 1000)   // `this` is the Timer
  }
}

Because arrows capture this lexically, the setInterval callback keeps the class instance's this — no bind needed. The flip side: you can't use an arrow as a method that needs its own this, or as a constructor (new throws).

IIFE — immediately invoked function expressions

An IIFE runs immediately and creates a private scope. Before block scoping and modules, it was the main tool for avoiding global pollution.

(function () {
  const secret = 42       // not visible outside
  console.log(secret)
})()

Modern code uses block scope (let/const) and ES modules instead, but you'll still see IIFEs in older code and for one-off async setup ((async () => { await init() })()).

Default parameters

Default parameters supply a value when an argument is undefined (not for other falsy values). Defaults can reference earlier parameters and are evaluated at call time.

function greet(name, greeting = 'Hello') {
  return `${greeting}, ${name}`
}
greet('Ada')              // 'Hello, Ada'
greet('Ada', undefined)   // 'Hello, Ada' — undefined triggers the default
greet('Ada', null)        // 'null, Ada' — null does NOT

Defaults are evaluated left to right, so a later default can use an earlier parameter: function range(start, end = start + 10) {...}.

Rest parameters

A rest parameter (...args) gathers any number of trailing arguments into a real array. It replaces the awkward arguments object for variadic functions.

function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0)   // nums is a real array
}
sum(1, 2, 3, 4)   // 10

function log(level, ...messages) {   // fixed param + rest
  console.log(level, messages.join(' '))
}

The rest parameter must be last, and unlike arguments, it's a genuine array with all the array methods available.

The arguments object

Inside regular (non-arrow) functions, arguments is an array-like object holding all passed arguments. It predates rest parameters and has quirks.

function old() {
  return Array.from(arguments).reduce((a, b) => a + b, 0)   // must convert first
}

arguments isn't a real array (no map/reduce directly), and arrow functions don't have it at all. Modern code should prefer rest parameters, which are cleaner and array-native. Reach for arguments only in legacy contexts.

Parameter destructuring

You can destructure objects or arrays right in the parameter list, often with defaults — a clean way to accept "options" objects.

function createUser({ name, role = 'user', active = true } = {}) {
  return { name, role, active }
}
createUser({ name: 'Ada' })          // { name: 'Ada', role: 'user', active: true }
createUser()                          // works — the `= {}` default prevents a crash

The trailing = {} is important: without it, calling createUser() with no argument would try to destructure undefined and throw. Always default the destructured parameter itself.

Function metadata: length and name

Functions expose a couple of introspective properties. length is the number of parameters before the first default or rest; name is the function's name (inferred for expressions).

function f(a, b, c = 1, ...rest) {}
f.length   // 2 — counts a, b only (stops at the default)
f.name     // 'f'

const g = () => {}
g.name     // 'g' — inferred from the variable

These are occasionally used by libraries (e.g. dependency injection inspecting arity), but mostly they're good to understand for debugging.

Methods vs standalone functions

A function stored as an object property is a method, and its this depends on how it's called. Detaching a method loses its this — a frequent bug fixed by arrow-function fields or bind.

const counter = {
  count: 0,
  increment() { this.count++ }   // method — `this` is `counter` when called as counter.increment()
}
const fn = counter.increment
fn()   // `this` is undefined (strict) — detached

Understanding that this is determined by the call site, not where the function is defined, is the key to avoiding these traps.

Key takeaways

  • Declarations are fully hoisted; expressions are not — pick based on whether you need to call before defining.
  • Arrow functions have no own this/arguments/super and can't be constructors — ideal for callbacks, wrong for methods.
  • Default parameters fill in for undefined only and can reference earlier params.
  • Rest parameters (...args) give a real array of extra arguments — prefer them over the array-like arguments object (absent in arrows).
  • Parameter destructuring with a = {} default cleanly handles options objects.
  • this is set by the call site; detached methods lose it — use arrows or bind.

Knowing each function form's hoisting and this behavior — plus the modern parameter features — lets you write functions that behave exactly as you intend, with no binding surprises.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.