JavaScript Function Parameters and Arguments: Defaults, Rest, Destructuring

Link copied
JavaScript Function Parameters and Arguments: Defaults, Rest, Destructuring

JavaScript Function Parameters and Arguments: Defaults, Rest, Destructuring

JS Tutorial Module 2: Functions Lesson 2.2

JavaScript is permissive about how functions receive their inputs. You can pass too few arguments or too many. You can default any parameter. You can collect leftovers into an array. You can destructure right in the signature so the function body never refers to the parameter object at all. Modern parameter syntax turns what used to be cluttered, defensive function bodies into self-documenting signatures.

This lesson covers every parameter feature: default values, rest parameters, destructuring patterns, the legacy arguments object, and the practical rules of thumb for designing clean function APIs.

The basics: parameters vs arguments #

function greet(greeting, name) {  // parameters
  return `${greeting}, ${name}!`;
}
greet('Hello', 'Ada');             // arguments
  • Parameters are placeholders defined when you write the function.
  • Arguments are the actual values passed when you call it.

JavaScript doesn't enforce a match. You can call with too few (missing parameters are undefined) or too many (extras are ignored unless captured by ...rest).

greet();              // 'undefined, undefined!'
greet('Hi');          // 'Hi, undefined!'
greet('Hi', 'Ada', 'extra'); // 'Hi, Ada!' — extra ignored

This is by design for forward compatibility — old code calling a new function that added optional parameters still works. It's also the source of many bugs. TypeScript catches it for you; plain JS does not.

Default parameter values #

Give any parameter a default with =:

function greet(greeting = 'Hello', name = 'stranger') {
  return `${greeting}, ${name}!`;
}

greet();              // 'Hello, stranger!'
greet('Hi');          // 'Hi, stranger!'
greet('Hi', 'Ada');   // 'Hi, Ada!'

Defaults trigger only on undefined #

This is critical. null, 0, '', and false are NOT defaulted.

greet(undefined, 'Ada');  // 'Hello, Ada!' — default kicks in
greet(null, 'Ada');        // 'null, Ada!' — null is passed through
greet('', 'Ada');          // ', Ada!' — empty string is passed through

For truly missing values, you'd typically need a defensive check anyway. The default-on-undefined rule lets callers be explicit when they want a falsy value.

Defaults can use earlier parameters #

function makeUrl(host = 'localhost', port = 8080, scheme = `${port === 443 ? 'https' : 'http'}`) {
  return `${scheme}://${host}:${port}`;
}

Each default is evaluated only when needed (lazy), in left-to-right order. You can reference any earlier parameter or any variable in scope.

Defaults are evaluated on every call #

For non-primitive defaults like objects and arrays, a fresh one is created per call:

function addItem(item, list = []) {
  list.push(item);
  return list;
}

addItem(1);  // [1]
addItem(2);  // [2] — fresh array each call, not [1, 2]

This avoids the classic Python mutable-default gotcha. In JavaScript, default expressions are re-evaluated each call.

Rest parameters #

The ...rest pattern collects the remaining arguments into a real array:

function sum(...nums) {
  return nums.reduce((s, n) => s + n, 0);
}

sum(1, 2, 3);     // 6
sum();             // 0
sum(...[1, 2, 3]); // 6 — spread arg, rest receives

Rules:

  • ...rest must be the last parameter.
  • It's a real array — .map, .filter, .length, all work.
  • It only includes arguments not bound to earlier named parameters:
function tagged(tag, ...items) {
  return `${tag}: ${items.join(', ')}`;
}
tagged('Numbers', 1, 2, 3); // 'Numbers: 1, 2, 3'

This is the modern replacement for arguments. Always prefer rest.

The legacy arguments object #

Inside regular (non-arrow) functions, arguments is an array-like object holding all passed arguments:

function legacy() {
  console.log(arguments.length);      // 3
  console.log(arguments[0]);           // 'a'
  // arguments.map(...)               // TypeError — not a real array
  Array.from(arguments).map(x => x);   // works after conversion
}
legacy('a', 'b', 'c');

Limitations vs rest:

  • Array-like, not an array — no .map/.filter directly.
  • Not available in arrow functions.
  • Includes ALL arguments, even ones bound to named parameters — leading to subtle bugs.

Use rest in new code. arguments shows up only when reading older codebases.

Destructuring parameters #

This is where modern parameter syntax really shines. Destructure right in the signature to give clean names to fields of an object or elements of an array.

Object destructuring #

function createUser({ name, age = 0, email, role = 'user' }) {
  // body uses name, age, email, role directly
}

createUser({ name: 'Ada', email: 'ada@x.com' });
// age defaults to 0, role to 'user'

Reads like a typed interface. The callee declares exactly what fields it needs.

For optional configs, default the whole parameter:

function connect({ host = 'localhost', port = 5432 } = {}) {
  // ...
}

connect();                    // safe — all defaults apply
connect({ host: 'db.x.com' });

Without the = {}, calling connect() would try to destructure undefined and throw.

Array destructuring #

Less common for parameters, but useful when the input is tuple-shaped:

function handlePair([first, second]) {
  return `${first}-${second}`;
}
handlePair(['a', 'b']);

Generally object destructuring reads better — named fields beat positional ones.

Renaming and nested destructuring #

function process({
  user: { name: userName },
  options: { timeout = 30 } = {},
}) {
  // userName and timeout are now local variables
}

Gets unwieldy fast. Aim for one or two levels at most in a signature.

Designing function signatures #

A few practical rules drawn from the patterns above:

1. Up to three positional parameters #

function publish(title, body, options) { /* ... */ }

Beyond three positions, callers lose track. Switch to an options object.

2. Options object for anything past three #

function request({ url, method = 'GET', headers = {}, body, signal, timeout = 5000 }) {
  // ...
}

Named parameters at the call site, defaults at the declaration. Refactor-safe — adding a new option doesn't reorder anything.

3. Required positional first, optional named after #

function download(url, { timeout = 5000, retry = true } = {}) { /* ... */ }

Url is required and clearly positional. Everything else is named with defaults.

4. Avoid boolean parameters #

// Bad — caller has to remember what `true` means
sendEmail(user, true);

// Better — named options
sendEmail(user, { skipQueue: true });

Named booleans read better at call sites and survive added options.

5. Use rest only for genuinely variadic functions #

function max(...nums) { return Math.max(...nums); }
log('info', 'Started');
log('error', 'Failed', error, requestId);

Don't use rest to fake an array — accept an array argument instead.

A worked refactor #

Before:

function createUser(opts) {
  if (!opts) opts = {};
  const name = opts.name;
  const age = opts.age !== undefined ? opts.age : 0;
  const active = opts.active !== undefined ? opts.active : true;
  const role = opts.role || 'user';
  // ...
}

After:

function createUser({ name, age = 0, active = true, role = 'user' } = {}) {
  // ...
}

Five lines of defensive parameter parsing replaced by one line of signature. Same behavior. Reader of the function sees the API immediately.

A summary checklist #

  • Default with = in parameters. Only triggers on undefined.
  • Use ...rest instead of arguments. Real array, works in arrows.
  • Destructure in the signature for object parameters. Hugely improves readability.
  • Default the destructured container (= {}) so calling without arguments doesn't throw.
  • Three positional max. Switch to an options object beyond that.
  • Named booleans beat positional ones.
  • TypeScript catches the arity mismatches that plain JS silently allows. Worth considering for any non-trivial codebase.

What's next #

That completes Module 2's gaps. With declarations vs arrows (Lesson 2.1) and parameters (this lesson) layered on top of the previously-published lessons on hoisting, scope, closures, this, higher-order patterns, and pure functions, Module 2 is the deepest treatment of JavaScript functions you'll find.

Next up in the tutorial path: Module 5 covers callbacks and async/await — the two remaining gaps in async land.

Try it yourself #

The destructured-with-default-{} pattern is the single most useful parameter trick. Predict the output:

YouPredict the output:
function connect({ host = 'localhost', port = 5432 } = {}) {
return `${host}:${port}`;
}
console.log(connect());
console.log(connect({ host: 'db.x.com' }));
console.log(connect({ port: 6543 }));
Claude · used js_sandboxOutput:
localhost:5432 (no args — outer = {} kicks in, both inner defaults apply)
db.x.com:5432 (host overridden, port default)
localhost:6543 (port overridden, host default)

One signature handles all four combinations. No defensive checks needed in the body.

That one-line pattern replaces an entire family of "set sensible defaults" helpers older codebases used to import from utility libraries.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *