Modern JavaScript gives you three tools for flexible arguments, and real codebases lean on all three daily. Default parameters fill in missing values, so callers can omit arguments they do not always need. Rest parameters gather every remaining argument into a real array, letting one function accept any number of inputs. Spread syntax expands a collection into separate items, whether you are calling a function, building an array, or cloning an object.
Together these replace most of the old arguments object hacks and manual array slicing. They make functions more readable at the call site and more robust at the definition site — and they share one piece of syntax, the three dots ..., which is exactly where beginners get tripped up, because the meaning of those dots flips depending on where you write them.
flowchart TD DEF["default params"] --> F1["fill missing values"] REST["rest ...args"] --> F2["collect into array"] SPR["spread ...arr"] --> F3["expand elements"] style DEF fill:#e0900b,color:#fff style REST fill:#e0900b,color:#fff style SPR fill:#e0900b,color:#fff
Default parameters
A default parameter assigns a fallback value when the argument is missing or explicitly undefined. It goes right in the function header:
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}`;
}
Now greet("Ada") returns "Hello, Ada", and greet("Ada", "Hi") returns "Hi, Ada". The default is skipped only when the argument is missing or undefined; passing null, 0, or "" uses those values instead.
Defaults are evaluated left to right, so a later parameter can use an earlier one:
function greet(name, greeting = "Hello, " + name) { ... }
One subtle rule: once a parameter has a default, every parameter to its right must also have one. JavaScript evaluates arguments positionally, and it would be ambiguous where to place a trailing positional argument if some earlier parameter were optional.
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}`;
}
console.log(greet("Ada"));
console.log(greet("Ada", "Hi"));
console.log(greet("Ada", undefined));
console.log(greet("Ada", null));
The default trap: undefined versus null
Beginners assume a default kicks in for any "empty" value. It does not — it kicks in for exactly one of them. Pass null, 0, an empty string, or false, and JavaScript keeps what you gave it; only a missing argument or a literal undefined lets the fallback fire. The reason is that null and undefined are different ideas in this language: null is an intentional absence, while undefined marks a slot that was never filled.
So a function that defaults a discount applies the default when you write price(item) or price(item, undefined), yet charges full price when you write price(item, null), because you told it, on purpose, that there is no discount.
flowchart LR A["sum(1, 2, 3)"] --> B["a = 1"] A --> C["b = 2"] A --> D["...nums = [3]"] style D fill:#e0900b,color:#fff
Rest parameters
Rest parameters gather the remaining arguments into a real array. They use the same ... syntax as spread, but in a parameter list they mean "collect everything else here":
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // nums is [1, 2, 3, 4]
Rest is not an exotic feature — it is just a cleaner replacement for the old arguments object. Unlike arguments, a rest parameter is a real array, so you can call .map, .filter, and .reduce on it directly.
You can mix regular parameters with rest, but rest must be the last parameter in the list. function(a, b, ...rest) is valid; function(...rest, a) is a syntax error because the engine would not know where positional arguments end and the rest begin.
function join(separator = ", ", ...parts) {
return parts.join(separator);
}
console.log(join(" - ", "a", "b", "c"));
console.log(join(undefined, "x", "y"));
function tag(first, ...rest) {
console.log(first, rest);
}
tag("hello", "world", "!");
Spread syntax
Spread wears the same three dots as rest, but it works in reverse: it expands an array or object into individual elements. Use it in function calls, array literals, or object literals:
const nums = [1, 2, 3];
console.log(Math.max(...nums)); // same as Math.max(1, 2, 3)
const copy = [...nums]; // a new array with the same items
Spread is indispensable for immutability. Instead of pushing into an array — which mutates it — you spread the old array into a new one and add the extra item:
const next = [...prev, 4]; // prev is untouched
For objects, spread copies own enumerable properties into a new object. It is the standard way to merge objects or override specific keys:
const defaults = { theme: "dark", fontSize: 14 };
const user = { fontSize: 16 };
const settings = { ...defaults, ...user }; // theme: dark, fontSize: 16
When two spread objects share a key, the one written last wins — that is how fontSize: 16 from user overrides the default.
flowchart LR A["[1, 2, 3]"] --> S["...nums"] S --> B["Math.max(1, 2, 3)"] S --> C["[0, ...nums, 4] [0,1,2,3,4]"] style B fill:#e0900b,color:#fff style C fill:#e0900b,color:#fff
const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([0, ...arr1, ...arr2]);
const base = { a: 1, b: 2 };
const override = { b: 9, c: 3 };
console.log({ ...base, ...override });
const items = ["x", "y"];
const more = [...items, "z"];
console.log(items);
console.log(more);
The same dots, two opposite jobs
... is the only new syntax in this lesson, and that is the whole trap, because it does opposite things depending on where it sits. Inside a parameter list, on the receiving end of a call, it gathers many arguments into one array — that is rest. Inside a call or a literal, on the giving end, it spreads one array or object out into many separate elements.
Read it as collect the rest here when it is a parameter, and as empty this array out here when it is an argument. If a line ever feels backwards, ask which side of the call it lives on, and the meaning snaps into place.
What does this print? Spread expands the array into individual arguments.
function maxOf(first, ...rest) {
return Math.max(first, ...rest);
}
console.log(maxOf(3, 1, 4, 1));
rest collects [1, 4, 1] into an array.
Math.max(3, ...[1, 4, 1]) is the same as Math.max(3, 1, 4, 1).
Write merge(base, ...overrides) that starts with base and spreads each object in overrides on top of it, left to right. Return the merged object. (Use object spread: {...obj1, ...obj2}.)
function merge(base, ...overrides) {
// return merged object
}
Start by copying base with spread.
Loop through overrides and spread each one onto the result.
function merge(base, ...overrides) {
let result = { ...base };
for (const obj of overrides) {
result = { ...result, ...obj };
}
return result;
}
Write prepend(prefix, ...words) that returns a new array where each word is prefixed with prefix and a space. Use map on the rest array. So prepend("Hi", "Ada", "Linus") returns ["Hi Ada", "Hi Linus"].
function prepend(prefix, ...words) {
// return array of prefixed strings
}
Use the rest parameter to collect words into an array.
Map over that array and build
${prefix} ${w}.
function prepend(prefix, ...words) {
return words.map((w) => `${prefix} ${w}`);
}
This function is meant to copy an array and append a new item without mutating the original. It mutates original instead. Fix it using spread.
function append(original, item) {
original.push(item);
return original;
}
Use spread to create a new array instead of mutating the original.
Return
[...original, item].
function append(original, item) {
return [...original, item];
}
Write defaults(target, ...sources) that copies all properties from each source object into target, left to right, using object spread. Earlier sources are overwritten by later ones. Return target.
function defaults(target, ...sources) {
// merge all sources into target, return target
}
Use a loop over the rest array
sources.Object.assign(target, src) copies properties into target in place.
function defaults(target, ...sources) {
for (const src of sources) {
Object.assign(target, src);
}
return target;
}
Recap
- Omit an argument, or pass
undefined, and the default fills in; passnullor0and your value is kept. - Rest (
...nameas a parameter) packs leftover arguments into a genuine array, and it must come last. - Spread (
...namein a call or literal) unpacks an array or object into its separate pieces. - Reach for spread to clone arrays and merge objects without mutating the originals.
- Every spread copy is one level deep: nested objects are shared, so editing one edits both.
Next you'll learn the difference between loose and strict equality, and why == has a reputation for surprises.