The JavaScript arguments object…and beyond

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

21 thoughts on “The JavaScript arguments object…and beyond

  1. 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/539974

    Notice, SM also has an issue with arguments: https://gist.github.com/578708

    Also, regarding hidden passed arguments (as in case with valueOf), as you know, FF’s setTimeout also passes the expired delay argument.

    Dmitry.

    1. Dmitry – I like your emulation of arguments vs. named argument vars – especially the way you use getter/setter to bind property of arguments to named argument variable

      Also I did not know about SM bug with arguments FD – will try to add to write-up

      thanks!

  2. 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’);

  3. Pingback: Vule Webstile

Leave a comment