Parameters and Arguments

Mental model: Parameters are the names you promise; arguments are the values that turn up. Defaults only fill the slots that are undefined.

Level: beginner · about 12 minutes

function greet(name = 'friend') {
  return `Hello, ${name}`;
}

console.log(greet('Ada'));        // → 'Hello, Ada'
console.log(greet());             // → 'Hello, friend'
console.log(greet(undefined));    // → 'Hello, friend'   the default fires
console.log(greet(null));         // → 'Hello, null'     null is a value

Look at the last two lines before you decide what a default does.

A default applies when the argument is undefined, and only then. null, 0, '' and false are all real values, so they are used as given. This is the single most common misunderstanding about defaults, and it is a feature: null usually means "deliberately nothing".

Defaults are expressions, evaluated at call time

let calls = 0;
const nextId = () => `id-${++calls}`;

function tag(label, id = nextId()) {   // runs per call, only when needed
  return `${label}:${id}`;
}

console.log(tag('a'));        // → 'a:id-1'
console.log(tag('b'));        // → 'b:id-2'
console.log(tag('c', 'own')); // → 'c:own'
console.log('nextId calls:', calls); // → 2

Parameters are initialised left to right, so a later default can use an earlier parameter. The reverse does not work: an earlier default cannot see a parameter that has not been initialised yet.

function box(width, height = width) {  // fine: width already exists
  return `${width}x${height}`;
}
console.log(box(3));      // → '3x3'
console.log(box(3, 5));   // → '3x5'

function broken(a = b, b = 2) { return [a, b]; }
try { broken(); } catch (err) { console.log(err.constructor.name); } // → 'ReferenceError'

Rest parameters collect the leftovers

function log(level, ...messages) {
  return `[${level}] ` + messages.join(' ');
}

console.log(log('warn', 'disk', 'almost', 'full')); // → '[warn] disk almost full'
console.log(log('info'));                           // → '[info] '

const parts = ['a', 'b'];
console.log(log('debug', ...parts));  // spread at the call site → '[debug] a b'

arguments, and why you can forget it

function old() {
  console.log(arguments.length);            // → 3
  console.log(Array.isArray(arguments));    // → false, it is array-like
  console.log(Array.from(arguments));       // → [1, 2, 3]
}
old(1, 2, 3);

const modern = (...args) => args.length;    // arrows have no `arguments`
console.log(modern(1, 2, 3));               // → 3

arguments is an array-like object available in every non-arrow function. It has length and indexes but none of the array methods, it does not include defaults you did not pass, and it does not exist inside arrows. Rest parameters give you a real array with a name, so prefer them in new code and recognise arguments when you read old code.

Destructured parameters and the options object

Positional: the call site is a riddle

createUser('Ada', true, false, 3);

function createUser(name, active, admin, tier) {
  // which boolean was which again?
}

Options object: the call site explains itself

createUser({ name: 'Ada', admin: false });

function createUser({ name, active = true,
                      admin = false, tier = 1 } = {}) {
  // order no longer matters
}

Once a function takes more than two or three arguments, or any two of them are booleans, switch to a single options object. You get names at the call site, defaults per key, and freedom to add options later without breaking callers.

function connect({ host = 'localhost', port = 5432, secure = false } = {}) {
  return `${secure ? 'https' : 'http'}://${host}:${port}`;
}

console.log(connect());                           // → 'http://localhost:5432'
console.log(connect({ port: 80 }));               // → 'http://localhost:80'
console.log(connect({ host: 'db', secure: true })); // → 'https://db:5432'

Note the = {} at the end. Without it, calling with no argument throws.

SituationShapeWhy
One or two obvious valuespositionaladd(a, b) needs no ceremony
A boolean flag or twooptions objectsave(doc, { force: true }) reads at the call site
Unknown number of same-typed valuesrest parametersum(...numbers)
Passing an existing array throughspread at the callsum(...totals)
Optional extras added over timeoptions object with = {}no breaking changes for callers
function scale(value, factor = 2) {
  return value * factor;
}
console.log(scale(5, undefined), scale(5, null));

undefined triggers the default, so the first call is 5 * 2. null does not, so the second is 5 * null, and null coerces to 0 in arithmetic, giving 0. Defaults guard against absence, not against every empty-looking value.

Try it yourself

Defaults, rest and spread together

function report(label = 'total', { prefix = '>', upper = false } = {}, ...values) {
  const sum = values.reduce((a, b) => a + b, 0);
  const line = `${prefix} ${label}: ${sum}`;
  return upper ? line.toUpperCase() : line;
}

console.log(report());
console.log(report('sales', {}, 10, 20, 30));
console.log(report('sales', { prefix: '*', upper: true }, 10, 20));

const figures = [1, 2, 3, 4];
console.log(report('spread', {}, ...figures));

Add a separator option. Then call report with an array using spread, and try passing null for label to see the default not fire.

Exercises

The options object pattern

Write createUser(options) which returns { name, role, active }. Destructure the options in the parameter list with defaults 'anonymous', 'member' and true, and make createUser() with no argument work. A caller who passes null for a value gets that null back.

Average any number of values

Write average(...numbers) which returns the mean of the arguments it receives, and 0 when it receives none. Callers should be able to spread an existing array into it.

Check yourself

What does this log?
1 0 1 — show() and show(undefined) both use the default array of one element. show([]) passes a real, empty array, so the default is not used and the length is 0. Absent and empty are different things.
Why is = {} needed in function f({ a = 1 } = {})?
So calling f() has an object to destructure instead of undefined — Destructuring reads properties from the argument. With no argument the value is undefined, and reading a property off undefined is a TypeError. The = {} supplies an empty object so every inner default can apply.
Which is true about arguments?
It is array-like, only in non-arrow functions, and rest parameters replace it — arguments has length and indexes but no array methods, and arrows do not create one. Array.from(arguments) converts it, but a rest parameter is clearer and gives the values a name.

Common mistakes

  • Expecting a default to fire for null. Only undefined triggers it.
  • Destructuring a parameter without = {}, so calling with no argument throws a TypeError.
  • Reaching for arguments inside an arrow function, which silently resolves to an outer one or throws.

Takeaways

  • Defaults fill undefined only, and they are evaluated at call time, left to right.
  • ... gathers in a parameter list and spreads at a call site.
  • Rest parameters give you a real array; arguments is the older, array-like version.
  • A destructured options object with = {} keeps call sites readable and callers unbroken.