Spare a thought for JavaScript’s arguments
object. It wants so desperately to be an array. It walks like an array, quacks like an array but flies like a turkey. During the early years of the language Brendan Eich came close to rewriting arguments
as an array until ECMA came along and clipped its wings forever.
In spite of all this (or maybe because of it) we love the arguments
object. In this article I’ll explore its niftyness and its quirkiness and I’ll finish up by looking at the likely successors: rest
and spread
…
The arguments object
When control enters the execution context of a function an arguments
object is created. The arguments
object has an array-like structure with an indexed property for each passed argument and a length
property equal to the total number of parameters supplied by the caller. Thus the length
of the arguments
object can be greater than, less than or equal to the number of formal parameters in the function definition (which we can get by querying the function’s length property):
function echoArgs(a,b) { return arguments; } //number of formal parameters... echoArgs.length; //2 //length of argument object... echoArgs().length; //0 echoArgs(5,7,8).length; //3
Binding with named function parameters
Each member of the arguments
object shares its value with the corresponding named parameter of the function – so long as its index is less than the number of formal parameters in the function.
ES5 clause 10.6 (note 1) puts it like this:
For non-strict mode functions the array index […] named data properties of an arguments object whose numeric name values are less than the number of formal parameters of the corresponding function object initially share their values with the corresponding argument bindings in the function’s execution context. This means that changing the property changes the corresponding value of the argument binding and vice-versa
(function(a) { console.log(arguments[0] === a); //true console.log(a); //1 //modify argument property arguments[0] = 10; console.log(a); //10 //modify named parameter variable a = 20; console.log(arguments[0]); //20 })(1,2)
Argument properties whose index is greater than or equal to the number of formal parameters (i.e. additional arguments which do not correspond to named parameters) are not bound to any named parameter value. Similarly if a function call does not supply an argument for every named parameter, the unfilled parameters should not be bound to the arguments
object and their values cannot be updated by modifying the arguments
objects…
//Invoke a three argument function but only pass two arguments (function(a, b, c) { //'arguments' has two members console.log(arguments.length); //2 //Updating arguments[2] should do not modify named param arguments[2] = 10; console.log(c); //undefined })(1,2); (function(a, b, c) { //Assigning to 'c' should not populate 'arguments' object c = 10; console.log('2' in arguments); //false })(1,2)
…well according to the ES5 spec at least. Unfortunately the Chrome browser doesn’t comply. It creates an arguments
member for every named parameter, regardless of whether the argument was actually passed (this is a known issue)
//CHROME BROWSER ONLY... (function(a, b, c) { //Updating arguments[2] should do not modify named param arguments[2] = 10; console.log(c); //10!! })(1,2); (function(a, b, c) { //Assigning to 'c' should not populate 'arguments' object c = 10; console.log('2' in arguments); //true!! })(1,2)
There’s another bug related to Chrome’s over-reaching arguments
object. Deleting supposedly non-existent members of the arguments
object will cause the corresponding named (but not passed) parameter value to be wiped out:
var cParam = (function(a, b, c) { c = 3; delete arguments[2]; return c; })(1,2); cParam; // Chrome -> undefined // Other browsers -> 3
arguments.callee
Every instance of arguments
has a callee
property which references the currently invoking function. The strict mode of ES5 disallows access to arguments.callee
arguments.caller
In supported implementations, every instance of arguments
has a caller
property which references the function (if any) from which the current function was invoked. There is only patchy vendor support for arguments.caller
and it is not standardized by ECMA except to explicitly disallow access in the strict mode.
More quirkiness
1) An arguments
object will not be created if arguments is the name of a formal parameter or is used as a variable or function declaration within the function body:
function foo(a, arguments) { return arguments; }; foo(1); //undefined function foo(a, b) { var arguments = 43; return arguments }; foo(1, 2); //43
2) The SpiderMonkey engine (used by Firefox) supplies a secret value at arguments[0]
when invoking valueOf
. The value will be “number” if the object is to be coerced to a number, otherwise undefined.
Thanks to Andrea Giammarchi for the following example
//FIREFOX BROWSER ONLY... var o = { push:[].push, length:0, toString:[].join, valueOf:function(){ return arguments[0] == "number" ? this.length : this.toString(); } }; o.push(1, 2, 3); o.toString(); // "1,2,3" (o*1).toString(); // 3
Arrays vs. arguments
As noted, the arguments
object is not an array. It is not a product of the Array constructor and it lacks all of the standard methods of Array. Moreover changing the length of arguments
has no effect on its indexed properties:
var arr = [1,2,3]; var args = echoArgs(1,2,3); Object.prototype.toString.apply(arr); //[object Array] Object.prototype.toString.apply(args); //[object Object] arr.push(4); //4 args.push(4); //TypeError: args.push is not a function arr.length = 1; arr[2]; //undefined args.length = 1; args[2]; //3
Leveraging methods of Array.prototype
Since all the methods of Array.prototype
are designed to be generic they can be easily applied to the array-compatible arguments
object:
var args = echoArgs(1,2,3); [].push.apply(args,[4,5]); args[4]; //5 var mapped = [].map.call(args, function(s) {return s/100}); mapped[2]; //0.03
A common approach is to go one better by using Array.prototype.slice
to copy the entire arguments
object into a real array:
var argsArray = [].slice.apply(echoArgs(1,2,3)); argsArray.push(4,5); argsArray[4]; //5 var mapped = argsArray.map(function(s) {return s/100}); mapped[2]; //0.03
Practical Applications
1. Functions that take unlimited arguments
var average = function(/*numbers*/) { for (var i=0, total = 0, len=arguments.length; i<len; i++) { total += arguments[i]; } return total / arguments.length; } average(50, 6, 5, -1); //15
2. Verifying all named arguments are supplied
JavaScript’s liberal attitude to parameter passing is appealing but some functions will break if all named arguments are not supplied. We could write a function wrapper to enforce this when necessary:
var requireAllArgs= function(fn) { return function() { if (arguments.length < fn.length) { throw(["Expected", fn.length, "arguments, got", arguments.length].join(" ")); } return fn.apply(this, arguments); } } var divide = requireAllArgs(function(a, b) {return a/b}); divide(2/5); //"Expected 2 arguments, got 1" divide(2,5); //0.4
3. A string formatter
(based on Dean Edwards’ Base 2 library)
function format(string) { var args = arguments; var pattern = RegExp("%([1-" + (arguments.length-1) + "])", "g"); return string.replace(pattern, function(match, index) { return args[index]; }); }; format("a %1 and a %2", "cat", "dog"); //"a cat and a dog"
4. Partial function application
The typical JavaScript implementations of curry, partial and compose store the arguments
object for later concatenation with the runtime arguments of the inner function.
Function.prototype.curry = function() { if (arguments.length<1) { return this; //nothing to curry with - return function } var __method = this; var args = [].slice.apply(arguments); return function() { return __method.apply(this, args.concat([].slice.apply(arguments))); } } var converter = function(ratio, symbol, input) { return [(input*ratio).toFixed(1),symbol].join(" "); } var kilosToPounds = converter.curry(2.2,"lbs"); var milesToKilometers = converter.curry(1.62, "km"); kilosToPounds(4); //8.8 lbs milesToKilometers(34); //55.1 km
The Future…
Brendan Eich has stated that the arguments
object will gradually disappear from JavaScript. In this fascinating “minute with Brendan” excerpt he ponders the future of arguments handling. Here’s my take away:
rest parameters
Harmony (the next scheduled specification of ECMAScript) has already penciled in the design for a likely successor known as a rest parameter and it’s scheduled to be prototyped in Firefox later this year (ActionScript already supports a similar feature).
The idea of behind the rest
parameter is disarmingly simple. If you prefix the last (or only) formal parameter name with ‘…’, that parameter gets created as an array (a genuine array) which acts as a bucket for all passed arguments that do not match any of the other named parameters.
Here’s a simple example…
//Proposed syntax.... var callMe(fn, ...args) { return fn.apply(args); } callMe(Math.max, 4, 7, 6); //7
…and here’s our curry function rewritten using rest
arguments. This time there is no need to copy the outer arguments
object, instead we make it a rest
parameter with a unique name so that the inner function can simply reference it by closure. Also no need to apply array methods to either of our rest
arguments.
//Proposed syntax.... Function.prototype.curry = function(...curryArgs) { if (curryArgs.length < 1) { return this; //nothing to curry with - return function } var __method = this; return function(...args) { return __method.apply(this, curryArgs.concat(args); } }
spread
Similar to Ruby’s splat
operator, spread
will unpack an array into a formal argument list. Amongst other things this allows the members of a rest
parameter to be passed as a set of formal arguments to another function:
//Possible future syntax.... var stats = function(...numbers) { for (var i=0, total = 0, len=numbers.length; i<len; i++) { total += numbers[i]; } return { average: total / arguments.length, max: Math.max(numbers); //spread array into formal params } } stats(5, 6, 8, 5); //{average: 6, max: 8}
Notice that I’m assuming that there will be no need for a formal spread
operator and that spread
just describes the process of automatic coercion from an array into listed parameters.
For the above example we could have fallen back on the traditional Math.max.apply(numbers)
instead, but unlike apply
spread will also work with constructors and with multiple array arguments.
A Brave New (JavaScript) World awaits…enjoy!
Further Reading
Brendan Eich: A minute with Brendan: the arguments argument
Nicholas C. Zakas: Mysterious arguments object assignments
Andrea Giammarchi: JavaScript arguments weirdness
ES wiki: harmony / rest_parameters
ECMA-262 5th Edition
10.6 Arguments Object
Annex C: The Strict Mode of ECMAScript
I remember the Chrome’s bug long time ago (at least in c.l.js I mentioned it at least an year ago, and it’s still in opened bugs). Probably you’ll be interested — correct abstract semantics of ES3/non-strict ES5
arguments
semantics: https://gist.github.com/539974Notice, SM also has an issue with
arguments
: https://gist.github.com/578708Also, regarding hidden passed arguments (as in case with
valueOf
), as you know, FF’ssetTimeout
also passes the expired delay argument.Dmitry.
Dmitry – I like your emulation of
arguments
vs. named argument vars – especially the way you use getter/setter to bind property ofarguments
to named argument variableAlso I did not know about SM bug with
arguments
FD – will try to add to write-upthanks!
I was writting an article about this topic on my blog when I read yours. Wow!, this is synchronicity…
After that, I have expanded my post to include some of your examples and your references.
As other times, it’s in Spanish, so I hope that it could be interesting for a few of yours readers.
http://www.etnassoft.com/2011/01/21/el-objeto-arguments-en-javascript
Great article, Angus.
Best regards!
Thanks – nice article!
The example with slicing the Arguments object is not working for me. This example code prints out ‘object’ where I expected it to print ‘array’:
(function(){
var args = [].slice.apply(arguments);
console.log(typeof args);
})(‘foo’,’bar’);
Reblogged this on rg443blog and commented:
JavaScript arguments object