JAVASCRIPT / FUNCTIONS
The arguments object and why rest replaced it
Read and convert the arguments object in older JavaScript, and replace it with rest parameters that give you a real array of just the unnamed values.
What you will learn
- Convert arguments to a real array with [...arguments] or Array.from(arguments)
- Replace Array.prototype.slice.call(arguments, 1) with a trailing rest parameter
- Explain why an arrow function reads the enclosing function's arguments object
- Spot non-strict aliasing between arguments[0] and its named parameter
Understanding The arguments object and why rest replaced it
Every call to an ordinary function, whether a declaration, a function expression, or a method, quietly creates a local binding named arguments. It is an object whose own keys are the strings "0", "1", "2" and so on plus length, and whose prototype is Object.prototype rather than Array.prototype. It exists because JavaScript shipped with no syntax for declaring a variable number of parameters, so the language handed every call a bag holding whatever the caller actually passed. The mental model that keeps you out of trouble is a record of the call, not a list of the extras.
That design is awkward in four specific ways. It has no array methods, so arguments.map is undefined and old code is littered with Array.prototype.slice.call(arguments, 1); it also contains the values that already have parameter names, so a variadic function must hard-code how many leading items to skip, and that count silently rots when the signature changes. The signature itself documents nothing, so a reader has to scan the whole body to learn what the function accepts. And in a non-strict function with a plain parameter list, arguments[i] and the matching named parameter are two views on one storage slot, so writing to one changes the other.
A rest parameter answers each of those points. The binding created by ...extras is a genuine Array, so Array.isArray is true and map and reduce are simply there, and it is filled from only the arguments left over after the named parameters, so nothing has to be sliced off the front. It sits in the signature where arity is readable, and it works inside arrow functions, which never get an arguments binding of their own. The arguments object was not removed and still shows up in legacy code and generic wrappers, so learn to read it, but do not build new code around it.
function describe(label) {
console.log(label, arguments.length, Array.isArray(arguments));
return Array.prototype.slice.call(arguments, 1).join("+");
}
function describeRest(label, ...extras) {
console.log(label, extras.length, Array.isArray(extras));
return extras.join("+");
}
console.log(describe("old", 1, 2, 3));
console.log(describeRest("new", 1, 2, 3));arguments is an array-like record of every value passed to an ordinary call, while a rest parameter is a real array of only the values you chose not to name.
Worked examples
Arrows have no arguments of their own
An arrow function that mentions arguments resolves it lexically to the enclosing ordinary function, while a rest parameter belongs to the arrow itself.
function outer(a, b) {
const inner = () => arguments.length;
return inner("x", "y", "z");
}
console.log(outer(1, 2));
function outerRest(a, b) {
const inner = (...innerArgs) => innerArgs.length;
return inner("x", "y", "z");
}
console.log(outerRest(1, 2));Example explained
Line 1inner is an arrow, so it creates no arguments binding and the identifier is looked up in outer's scope.
Line 2outer was called with two values, so arguments.length is 2 even though inner received three.
Line 3innerArgs is declared on the arrow, so it collects the arrow's own three arguments and reports 3.
Line 4This is why any variadic arrow must use a rest parameter; there is no arguments fallback.
Aliasing between arguments and named parameters
In a non-strict function with a plain parameter list, arguments indices and parameter names share storage; a default value or strict mode breaks the link.
function sloppy(x) {
arguments[0] = 99;
return x;
}
function unmapped(x = 0) {
arguments[0] = 99;
return x;
}
function strictFn(x) {
"use strict";
arguments[0] = 99;
return x;
}
console.log(sloppy(5), unmapped(5), strictFn(5));Example explained
Line 1sloppy has a simple parameter list in non-strict code, so arguments[0] and x are one slot and the write reaches x.
Line 2unmapped has a default value, which forces an unmapped arguments object, so x keeps 5 and only the object changes.
Line 3strictFn writes without error but the write is local to the arguments object, so the parameter is untouched.
Line 4Run this file as an ES module and the first value becomes 5, because module code is always strict.
Forwarding without apply
The slice-plus-apply pattern that arguments forces on you collapses into a rest parameter plus a spread call.
function callMath(name) {
var args = Array.prototype.slice.call(arguments, 1);
console.log(name + "(" + args.join(", ") + ")");
return Math[name].apply(Math, args);
}
const callMathRest = (name, ...args) => {
console.log(`${name}(${args.join(", ")})`);
return Math[name](...args);
};
console.log(callMath("max", 3, 7, 2));
console.log(callMathRest("min", 3, 7, 2));Example explained
Line 1slice.call(arguments, 1) is needed because arguments[0] is name, which must not be forwarded as a number.
Line 2apply was the only way to turn that array back into separate arguments before spread existed.
Line 3args in callMathRest already excludes name, so join and the spread call both work with no preprocessing.
Line 4Both versions produce identical results, so this rewrite is mechanical and safe.
Important notes
arguments is iterable, so [...arguments] and Array.from(arguments) both yield a real array even though arguments.map does not exist.
arguments.callee throws in strict mode, which is one more reason legacy recursion tricks built on it cannot be carried into modern code.
Common mistakes
Reaching for arguments inside an arrow function: you silently read the enclosing function's arguments, or get a ReferenceError if there is no enclosing function.
Calling arguments.map or arguments.filter: a TypeError, because arguments inherits from Object.prototype and has no array methods.
Forgetting that arguments still holds the named parameters, so [...arguments] inside function tag(name) includes name and the tag name ends up inside the content.
Try it yourself
Change, predict, then run
Write function tag(name) that returns "<p>a b</p>"-style markup by slicing arguments from index 1, then rewrite it as tag(name, ...children) and confirm tag("p", "a", "b") gives the same string from both versions.
Open the JavaScript workspaceCheck your understanding
function f(a) { const g = () => arguments.length; return g(1, 2, 3); } What does console.log(f(7, 8)) print?
- 3
- 2
- 1
- undefined
Show answer
The arrow g never creates its own arguments binding, so the identifier resolves lexically to f's arguments object, and f was called with two values. 3 is tempting because g is called with three arguments, but those values are only reachable through named parameters or a rest parameter on g.