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 valueLook 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.
| Situation | Shape | Why |
|---|---|---|
| One or two obvious values | positional | add(a, b) needs no ceremony |
| A boolean flag or two | options object | save(doc, { force: true }) reads at the call site |
| Unknown number of same-typed values | rest parameter | sum(...numbers) |
| Passing an existing array through | spread at the call | sum(...totals) |
| Optional extras added over time | options 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()andshow(undefined)both use the default array of one element.show([])passes a real, empty array, so the default is not used and the length is0. Absent and empty are different things. - Why is
= {}needed infunction f({ a = 1 } = {})? - So calling
f()has an object to destructure instead ofundefined— Destructuring reads properties from the argument. With no argument the value isundefined, and reading a property offundefinedis aTypeError. 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 —
argumentshaslengthand 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. Onlyundefinedtriggers it. - Destructuring a parameter without
= {}, so calling with no argument throws a TypeError. - Reaching for
argumentsinside an arrow function, which silently resolves to an outer one or throws.
Takeaways
- Defaults fill
undefinedonly, 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;
argumentsis the older, array-like version. - A destructured options object with
= {}keeps call sites readable and callers unbroken.