What is the Javascript Arguments Object Anyway?
by Jon Thacker Tuesday, June 21st, 2011While on the path to figuring out a mysterious issue in regards to my Javascript function working properly in Chrome but falling flat on its face in Firefox, I discovered an interesting difference between the two browsers. I’m going to add Safari into the mix as well.
// Chrome 12.0.742.91// Safari 5.0.5 (6533.21.1)// Firefox 3.6.17, 4.0.1
// Given the following functions:var getArgs = function() { return arguments;}
// Convert to an array using a for-in loopvar getWithForIn = function(args) { var arr = [] for (var i in args) { arr.push(args[i]) } return arr}
// Convert to an array using a for loopvar getWithFor = function(args) { var arr = [] for (var i=0; i<args.length; i++) { arr.push(args[i]) } return arr}
var args = getArgs('a','b','c')
// First a normal for loopgetWithFor(args)// [All] > ['a','b','c']
// Now with a for in loopgetWithForIn(args)// [Chrome, Firefox4] > ['a','b','c']// [Safari, Firefox3] > []
So whats all this about? Trying to iterate over an arguments object using a for-in loop does not work in Safari but works in Chrome.
According to the ECMA-262 specification, a for-in loop iterates over an object’s properties that have enumerable set to true. Using Object.getOwnPropertyNames and Object.getOwnPropertyDescriptor we can see if arguments is setting the fields properly. Firefox 3 is excluded from this list because it doesn’t have the required methods.
// Firefox3 is missing the Object functions needed to investigate itObject.getOwnPropertyNames(args)// [Chrome, Safari, Firefox4] > ['0', '1', '2', 'callee', 'length']
Object.getOwnPropertyDescriptor(args, 0).enumerable// [Chrome, Firefox4] > true// [Safari] > false
The properties ‘callee’ and ‘length’ both have enumerable set to false which keeps them from showing up in the for-in iteration in all browsers. In Chrome and Firefox 4, the arguments have enumerable set to true, while Safari does not. This seems to go against the specification, which states that when constructing an arguments object, each of the arguments should be set as properties on the object with the name of the property equal to the 0-based index of the argument and have the enumerable property set to true.
Another issue with using the Arguments object is when default values for the function’s arguments are used.
var getArgsWithDefaults = function(a, b, c) { a = 'a'; b = 'b'; c = 'c' return arguments}
var args = getArgsWithDefaults()
args.length// [Chrome, Safari, Firefox3] > 0
getWithFor(args)// [All] > []// Makes sense, zero length -> no iteration
getWithForIn(args)// [Safari, Firefox3, Firefox4] > []// [Chrome] > ['a','b','c']
Object.getOwnPropertyNames(args)// [Safari, Firefox4] > ['callee', 'length']// [Chrome] > ['0', '1', '2', 'callee', 'length']
Safari and Firefox seem to have taken the stance that its not an argument unless the function is called with a value, even if its undefined.
args = getArgsWithDefaults(undefined)
getWithFor(args)// [Safari, Firefox3, Firefox4] > ['a']// [Chrome] > ['a']
getWithForIn(args)// [Safari, Firefox3] > []// [Firefox4] > ['a']// [Chrome] > ['a','b','c']Conclusion: use a regular for loop with Arguments objects! The Arguments object is clearly distinct from an Array or a regular Object. It doesn’t have the same prototypes as the other two nor does it behave in the same was as the others when being iterated over. Its implemented as a separate object in each javascript implementation as well, which is required as it must be able to interact with the stack for pulling values for the arguments in a function. Clearly you can’t rely on a consistent behavior across browsers when theres default arguments involved either. Another option would be to convert the Arguments object to an Array or a regular Object for cross browser consistency.
At least in this case, Chrome definitely seems to come closest to meeting the specification while also implementing the expected behavior where it is a bit less well defined.
To quickly change the arguments object into an array, use:
var args = jQuery.makeArray( arguments );
or
var args = Array.prototype.slice.call( arguments );