Parameters: defaults, rest and spread

Beginner35 min

Use default parameters, rest parameters, spread in calls, destructured parameters and options objects.

Builds on
What you will learn
  • Give parameters default values and know when the default applies
  • Collect extra arguments with a rest parameter
  • Spread arrays into calls and copy arrays and objects with spread
  • Accept an options object with destructuring
  • Explain how arguments are passed
On this page

How much a function can adapt to its caller depends on its parameters. JavaScript is relaxed about them, and modern syntax adds several features that replace older patterns. The fundamentals lesson on functions explains parameters and arguments in general. Here is how JavaScript handles them.

Missing and extra arguments

JavaScript does not check how many arguments you pass. Missing parameters are undefined. Extra arguments are ignored.

arg_counts.mjs
function show(a, b, c) {
return [a, b, c];
}
console.log(show(1));
console.log(show(1, 2, 3, 4, 5));
console.log(show.length);
Output
[ 1, undefined, undefined ]
[ 1, 2, 3 ]
3

This flexibility is why default values and rest parameters exist. The length property of a function counts its declared parameters: 3 here.

Default parameters

A default value applies when the argument is undefined, either missing or passed explicitly as undefined (ES2015).

default_params.mjs
function greet(name = "friend", greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet());
console.log(greet("Ada"));
console.log(greet(undefined, "Hi"));
console.log(greet(null));
console.log(greet(""));
function rectangle(width, height = width) {
return { width, height };
}
console.log(rectangle(4), rectangle(4, 2));
let calls = 0;
function nextId(id = ++calls) {
return id;
}
console.log(nextId(), nextId(), nextId(99), nextId());
Output
Hello, friend!
Hello, Ada!
Hi, friend!
Hello, null!
Hello, !
{ width: 4, height: 4 } { width: 4, height: 2 }
1 2 99 3
  • greet() and greet(undefined, "Hi") use the defaults.
  • greet(null) does not. Only undefined triggers a default, so null and "" are kept as given. This is the same rule as ?? from the null and undefined lesson, except that ?? also treats null as missing.
  • A default can use earlier parameters, as height = width does.
  • A default is calculated each time the function is called, and only when needed. In nextId, the counter moves on only when no id is given, which is why passing 99 leaves it alone.

Put parameters that have defaults last, so that callers can leave them out.

Rest parameters

A rest parameter gathers the remaining arguments into a real array. It must be the last parameter and is written with three dots.

rest_spread.mjs
function sum(...numbers) {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
}
console.log(sum(), sum(1), sum(1, 2, 3));
function log(level, ...messages) {
console.log(`[${level}]`, messages.join(" "));
}
log("info", "server", "started", "on", 8080);
Output
0 1 6
[info] server started on 8080

The same three dots also work in a call and in a list literal, where they are called spread:

rest_spread.mjs (continued)
const scores = [72, 91, 85];
console.log(Math.max(...scores), Math.min(...scores));
console.log(sum(...scores, 10));
const copy = [...scores, 100];
console.log(copy, scores);
Output
91 72
258
[ 72, 91, 85, 100 ] [ 72, 91, 85 ]

And in a record literal:

rest_spread.mjs (continued)
const defaults = { theme: "light", size: 12 };
const settings = { ...defaults, size: 16 };
console.log(settings, defaults);
Output
{ theme: 'light', size: 16 } { theme: 'light', size: 12 }

sum(...numbers) accepts any number of arguments, including none. log takes one fixed parameter and collects the rest.

Older code used a special arguments object for this. It is array-like but not an array, it does not exist inside arrow functions, and its use is discouraged. Use a rest parameter instead.

Spread

The same three dots do the opposite when you write them in a call or in an array or object literal: spread unpacks a collection into separate items.

In the example above:

  • Math.max(...scores) passes each score as a separate argument. Math.max(scores) with an array would give NaN.
  • sum(...scores, 10) mixes spread items with ordinary ones.
  • [...scores, 100] builds a new array with a copy of the items, and the original stays unchanged.
  • { ...defaults, size: 16 } copies an object’s own properties into a new object, then overrides size. When two sources have the same key, the later one wins. Spread in object literals arrived in ES2018.

Spread makes a shallow copy: nested objects and arrays are shared between the copy and the original.

Destructured parameters and options objects

When a function takes more than two or three inputs, or several are optional, calls like createUser("ada", true, false) are hard to read. Pass one options object and take it apart in the parameter list with destructuring.

destructured_params.mjs
function formatName({ first, last, upper = false } = {}) {
const full = `${first ?? ""} ${last ?? ""}`.trim();
return upper ? full.toUpperCase() : full;
}
console.log(formatName({ first: "Ada", last: "Lovelace" }));
console.log(formatName({ last: "Hopper", first: "Grace", upper: true }));
console.log(formatName({ first: "Linus" }));
console.log(JSON.stringify(formatName()));
function distance([x1, y1], [x2, y2]) {
return Math.hypot(x2 - x1, y2 - y1);
}
console.log(distance([0, 0], [3, 4]));
Output
Ada Lovelace
GRACE HOPPER
Linus
""
5

The braces in { first, last, upper = false } pick named properties out of the argument, and each can have its own default. The = {} at the end is a default for the whole argument, so formatName() with nothing passed does not fail by trying to destructure undefined. Arrays can be taken apart too, as distance does with [x1, y1]. That is the same destructuring idea, and lesson 27 teaches it in full.

A common shape is one or two required positional parameters followed by an options object:

options_object.mjs
function createUser(name, { admin = false, active = true } = {}) {
return { name, admin, active };
}
console.log(createUser("ada"));
console.log(createUser("grace", { admin: true }));
console.log(createUser("linus", { active: false, admin: true }));
Output
{ name: 'ada', admin: false, active: true }
{ name: 'grace', admin: true, active: true }
{ name: 'linus', admin: true, active: false }

The caller names what they set, order does not matter, and new options can be added later without breaking existing calls.

How arguments are passed

Every argument is passed as a copy of a value. For primitives, such as numbers and strings, that is a copy of the value, so changing the parameter does not touch the caller’s variable. For objects and arrays, the copy is of the reference, so the function sees the same object. The general idea is in the fundamentals lesson on memory and references.

pass_by_sharing.mjs
function reassign(value) {
value = "changed";
return value;
}
let text = "original";
reassign(text);
console.log(text);
function addItem(list) {
list.push("new");
}
const items = ["old"];
addItem(items);
console.log(items);
function replaceList(list) {
list = ["replacement"];
}
replaceList(items);
console.log(items);
Output
original
[ 'old', 'new' ]
[ 'old', 'new' ]
  • reassign changes only its own value variable.
  • addItem changes the shared array, so the caller sees the new item.
  • replaceList assigns a new array to its own list variable, which does not affect the caller’s.

Functions that change their arguments surprise callers. When you can, return a new value instead.

Common pitfalls

  • Expecting null to trigger a default. Only undefined does.
  • Putting a rest parameter anywhere but last. It is a SyntaxError.
  • Forgetting = {} on a destructured parameter. Calling the function with no argument then throws a TypeError.
  • Mutating an argument. The caller’s array or object changes. Copy first with spread if you need to modify.
  • Assuming spread copies deeply. It copies one level.
  • Using arguments. Prefer rest parameters.

Exercises

ExerciseA flexible formatName

Write formatName({ first, last, upper }) that returns the first and last name joined by a space. Either name may be missing, and upper: true makes the result upper case. With no argument at all, it should return an empty string. Test with { first: "Ada", last: "Lovelace" }, { first: "Grace", last: "Hopper", upper: true }, { last: "Turing" } and no argument. Print the last result with JSON.stringify. You should see Ada Lovelace, GRACE HOPPER, Turing and "".

Show solution
format_name_solution.mjs
function formatName({ first = "", last = "", upper = false } = {}) {
const full = `${first} ${last}`.trim();
return upper ? full.toUpperCase() : full;
}
console.log(formatName({ first: "Ada", last: "Lovelace" }));
console.log(formatName({ first: "Grace", last: "Hopper", upper: true }));
console.log(formatName({ last: "Turing" }));
console.log(JSON.stringify(formatName()));
Output
Ada Lovelace
GRACE HOPPER
Turing
""

The defaults for first and last are empty strings, so a missing name does not print undefined. Joining the two names with a space and calling trim() removes the stray space when one of them is missing. The = {} makes the no-argument call work, and JSON.stringify prints "" so you can see that the result is an empty string.

Exerciseaverage and clamp

Write average(...numbers), which returns the mean of any number of arguments, or 0 when there are none. Then write clamp(value, { min, max }), which limits value to the range from min to max, with defaults of 0 and 100 and a default for the whole options object. Check these calls:

  • average(), average(4) and average(2, 4, 9) should give 0, 4 and 5.
  • average(...[10, 20, 30, 40]) should give 25. The brackets here are a list of four numbers, and the dots spread them into four arguments.
  • clamp(150), clamp(-5) and clamp(42) should give 100, 0 and 42.
  • clamp(7, { min: 10 }) and clamp(7, { max: 5 }) should give 10 and 5.
Show solution
average_clamp.mjs
function average(...numbers) {
if (numbers.length === 0) {
return 0;
}
let total = 0;
for (const n of numbers) {
total += n;
}
return total / numbers.length;
}
function clamp(value, { min = 0, max = 100 } = {}) {
return Math.min(Math.max(value, min), max);
}
console.log(average(), average(4), average(2, 4, 9));
console.log(average(...[10, 20, 30, 40]));
console.log(clamp(150), clamp(-5), clamp(42));
console.log(clamp(7, { min: 10 }), clamp(7, { max: 5 }));
Output
0 4 5
25
100 0 42
10 5

The rest parameter makes numbers a real array, so numbers.length tells you how many arguments came in, and the empty case is handled before dividing by zero. In clamp, Math.max(value, min) lifts a value that is too low, and Math.min(..., max) pulls down one that is too high. Each option has its own default, and the = {} lets you call clamp(150) with no options at all.

Summary

  • Missing arguments are undefined and extra ones are ignored.
  • A default applies only when the argument is undefined, and is evaluated at each call.
  • ...rest gathers extra arguments into an array. ...spread unpacks an array into a call or literal, or an object into an object literal.
  • Take an options object apart in the parameter list, with defaults, and add = {} so it can be omitted.
  • Objects are passed by sharing: the function can change what the caller’s object contains, but not which object the caller holds.

Next: how functions remember the variables around them.