JavaScript Function Parameters
A function parameter is a named placeholder you list inside a function’s parentheses; when the function is called, the values you pass in — the arguments — are bound to those names for the duration of the call. Parameters are how data gets into a function, and JavaScript’s parameter system is more flexible than in many languages: you can give a parameter a default value, collect any number of extra arguments into an array, or unpack an object or array straight into named variables as part of the parameter list itself. Getting comfortable with all of these forms — and knowing exactly when a default kicks in, how objects behave when passed as arguments, and how the old arguments object relates to modern parameters — will save you from a whole category of subtle bugs.
Overview: How Function Parameters Work
Parameters vs. Arguments
The terms are often used interchangeably, but there’s a useful distinction: parameters are the names declared in the function definition (function add(a, b) — a and b are parameters). Arguments are the actual values supplied at the call site (add(2, 3) — 2 and 3 are arguments). When the function runs, each argument is matched positionally to the corresponding parameter.
JavaScript Never Enforces Arity
Unlike many statically typed languages, JavaScript does not check that the number of arguments matches the number of declared parameters. You can call a function with too few arguments — the missing parameters are simply bound to undefined — or too many, in which case the extras are still accessible (via arguments or a rest parameter) but simply have no matching name. This is intentional flexibility, but it also means JavaScript won’t catch a missing-argument bug for you at call time; you have to guard for it yourself, often with default parameters.
Parameters Are Local Bindings
Each parameter behaves like a let-scoped variable local to the function body. It exists only for the duration of that call, is not visible outside the function, and a new set of bindings is created every single time the function is invoked. If a parameter has a default value, that default expression is evaluated left-to-right at call time, and — importantly — it can reference earlier parameters in the list, because by the time it runs those earlier parameters have already been initialized. Referencing a later parameter from an earlier default throws a ReferenceError because that later variable is still in its “temporal dead zone,” exactly like referencing a let before its declaration line.
Value Semantics: Primitives vs. Objects
JavaScript always passes arguments by value. For primitives (numbers, strings, booleans, etc.) that value is a copy, so reassigning a parameter inside the function never affects the caller’s variable. For objects and arrays, the “value” being copied is a reference to the same underlying object. That means reassigning the parameter itself (cart = []) does not affect the caller, but mutating the object through that reference (cart.push(item)) absolutely does — the caller sees the change, because both the caller’s variable and the parameter point at the same object in memory.
Function Arity and .length
Every function has a .length property equal to the number of parameters declared before the first one with a default value or a rest parameter. So function f(a, b, c = 1, ...rest) {} has f.length === 2. This quirk exists because .length is meant to describe how many arguments the function “expects” in the strict sense — defaulted and rest parameters are optional by design, so they don’t count.
Syntax
function functionName(param1, param2 = defaultValue, ...restParams) {
// param1, param2, and restParams are available here
}
param1— a plain positional parameter; receives whatever argument is passed in that slot, orundefinedif none was given.param2 = defaultValue— a parameter with a default value, used only when the corresponding argument isundefined(including when it’s simply omitted)....restParams— a rest parameter that gathers all remaining arguments into a real array. It must be the last parameter in the list.
Two other parameter forms unpack values directly in the parameter list:
| Form | Example | Use case |
|---|---|---|
| Object destructuring | function f({ id, name = "N/A" }) {} |
Named/optional arguments, order doesn’t matter at the call site |
| Array destructuring | function f([first, second]) {} |
Unpacking fixed-position tuples like coordinates |
| Rest parameter | function f(...items) {} |
Variable-length argument lists |
| Default value | function f(x = 10) {} |
Optional parameters with a sensible fallback |
Examples
Example 1: Basic Positional Parameters
function greet(name, greeting) {
console.log(`${greeting}, ${name}!`);
}
greet("Ava", "Hello");
greet("Ben");
Output:
Hello, Ava!
undefined, Ben!
The first call supplies both arguments, so they’re matched positionally: name = "Ava", greeting = "Hello". The second call only supplies one argument, which fills name; since no second argument was passed, greeting is bound to undefined and gets printed as the literal text “undefined” inside the template literal. This is exactly the kind of gap that default parameters exist to close.
Example 2: Defaults, Destructuring, and Rest Together
function createUser(name, { age = 18, role = "member" } = {}, ...tags) {
console.log(`${name} (${age}) - ${role}`);
console.log(tags);
}
createUser("Priya");
createUser("Sam", { age: 25, role: "admin" }, "vip", "beta");
Output:
Priya (18) - member
[]
Sam (25) - admin
[ 'vip', 'beta' ]
The second parameter is destructured straight out of an options object, and that object itself defaults to {} so calling createUser("Priya") without a second argument doesn’t throw (destructuring undefined would throw a TypeError — the = {} fallback prevents that). Inside that object, age and role each have their own defaults. Any arguments after the second are swept into the tags rest array, which is [] when none are supplied.
Example 3: Defaults Referencing Earlier Parameters
function computeTotal(price, quantity = 1, tax = price * 0.1) {
return (price * quantity) + tax;
}
console.log(computeTotal(100));
console.log(computeTotal(100, 3));
console.log(computeTotal(100, 3, 20));
Output:
110
310
320
The default for tax is an expression, price * 0.1, evaluated fresh on every call that omits it — and it’s allowed to reference price because price is initialized earlier in the parameter list. In the first call, quantity and tax both fall back to their defaults: 1 and 100 * 0.1 = 10, giving 100 * 1 + 10 = 110. The second call supplies quantity explicitly but still defaults tax. The third call supplies all three, so no defaults run at all.
Under the Hood: Parameters and the arguments Object
Before rest parameters existed, functions relied on a special array-like object called arguments, automatically available inside every non-arrow function, holding every argument passed in, regardless of how many named parameters were declared. It still exists today and still works — but its relationship to named parameters is easy to get wrong.
For a function with a simple parameter list (no defaults, rest, or destructuring) running in non-strict mode, the engine creates a mapped arguments object: arguments[0] and the first named parameter are literally two views onto the same storage slot, so changing one changes the other. But the moment a function uses any default value, rest parameter, or destructured parameter, the spec requires an unmapped arguments object instead — arguments becomes a plain snapshot of what was passed, completely disconnected from the named parameters from that point on.
function simple(a, b) {
a = 99;
console.log(arguments[0]);
}
simple(1, 2);
function withDefault(a = 10, b) {
a = 99;
console.log(arguments[0]);
}
withDefault(1, 2);
Output:
99
1
simple has a simple parameter list, so reassigning a also updates arguments[0], printing 99. withDefault has a default parameter, which forces an unmapped arguments object, so arguments[0] stays frozen at the original argument, 1, even after a is reassigned. This is a genuine behavioral difference baked into the language, not a bug — one more reason modern code favors rest parameters over arguments.
Common Mistakes
Mistake 1: Assuming Defaults Trigger on Any Falsy Value
A default parameter only activates when the argument is undefined — not for null, 0, "", or false. Passing null explicitly is treated as a real value, not “nothing.”
function setVolume(level = 50) {
console.log(level);
}
setVolume(null);
setVolume(undefined);
setVolume(0);
Output:
null
50
0
If you actually want null to also fall back to the default, handle it explicitly inside the function, for example with the nullish coalescing operator: level = level ?? 50.
Mistake 2: Expecting Arrow Functions to Have Their Own arguments
Arrow functions do not get their own arguments object. Any reference to arguments inside an arrow function resolves to the nearest enclosing regular function’s arguments — which is rarely what you intended.
function outer() {
const arrow = () => {
console.log(arguments[0]);
};
arrow(99);
}
outer("real-arg");
Output:
real-arg
Even though arrow is called with 99, arguments[0] inside it refers to outer‘s arguments, so it prints “real-arg”, not 99. If an arrow function needs access to its own call arguments, give it an explicit rest parameter instead: const arrow = (...args) => {...}.
Mistake 3: Not Realizing Object Parameters Are Shared References
Because objects and arrays are passed by reference, mutating one inside a function changes the original the caller holds, which can produce surprising side effects if you weren’t expecting the function to modify its input.
function addItem(cart, item) {
cart.push(item);
}
const myCart = ["apple"];
addItem(myCart, "banana");
console.log(myCart);
Output:
[ 'apple', 'banana' ]
This may be exactly what you want (an intentional mutation), but if you meant to leave the caller’s array untouched, make a copy first — for example function addItem(cart, item) { return [...cart, item]; } — and return a new array instead of mutating the one you were given.
Best Practices
- Prefer default parameters (
function f(x = 10)) over the oldx = x || 10pattern, which incorrectly overrides legitimate falsy values like0or"". - Use a rest parameter (
...args) instead of the legacyargumentsobject — it’s a real array (so.map,.filter, etc. work directly) and it behaves consistently across arrow and regular functions. - When a function needs more than two or three optional inputs, accept a single destructured options object instead of a long, hard-to-remember positional list.
- Always give an options object parameter a default of
{}({ a, b } = {}) so the function doesn’t throw when called with no argument in that position. - Put required parameters first and optional/defaulted ones last — parameters with defaults that come before required ones must still be explicitly passed as
undefinedto “skip” them, which is awkward and easy to get wrong. - Avoid mutating parameters you receive unless mutation is explicitly the function’s documented contract; return new values instead when possible.
- Use
??(nullish coalescing) inside the function body when you need a fallback for bothnullandundefined, since default parameters alone only coverundefined.
Practice Exercises
- Write a function
power(base, exponent = 2)that returnsbaseraised toexponent. Call it aspower(5)andpower(2, 10)and predict both results before running it. - Write a function
formatName({ first, last, middle = "" })that returns a full name string, correctly handling the case wheremiddleis omitted (no double space should appear in the output). - Write a function
sum(...numbers)that uses a rest parameter andreduceto add up any number of arguments, including zero arguments (which should return0).
Summary
- Parameters are the names in a function’s definition; arguments are the actual values passed at the call site, matched to parameters positionally.
- JavaScript never enforces arity — missing arguments become
undefined, and extra arguments are still reachable via rest parameters orarguments. - Default parameters only activate for
undefined, not fornullor other falsy values, and can reference earlier parameters in the same list. - Rest parameters (
...name) collect remaining arguments into a real array and must come last; destructured parameters unpack objects or arrays directly in the signature. - Primitives are copied when passed as arguments; objects and arrays are passed as shared references, so mutating them inside a function affects the caller.
- The
argumentsobject is mapped to named parameters only in simple-parameter, non-strict functions — any default, rest, or destructured parameter makes it unmapped, and arrow functions have noargumentsof their own.
