Sunday, June 21, 2009

JavaScript arguments Weirdness!

As you know, arguments is a "magic undeclared variable" with a local scope present in each function body. This variable is an Object, with an Array like structure.


checkArgs(1, 2, 3);

function checkArgs(){
// even if we do not declare
// arguments, this one will
// be present with a length property
// equal to 3 and respective
// 1, 2, and 3 values at index
// 0, 1, and 2
arguments.length; // 3
arguments[0]; // 1
Array.prototype.join.call(arguments,"-");
// will produce 1-2-3
};

Nothing new so far, but I bet not everybody knew some arguments "feature" which is browser dependent or somehow re-usable.

The Common "For In" Behavior

There is a particular behavior about arguments variable, it does not expose properties in a "for in" loop.


checkArgs(1, 2, 3);

function checkArgs(){
for(var key in arguments)
alert(key);
// nothing happened ...
};
How can be possible? Simple enough, 0, 1, and 2 index properties are not user defined but JavaScript core assignments. Since JavaScript nature is extremely dynamic, it is simple to define what you want to expose in a loop, so far user defined properties or methods, and what should not be iterated in a "for in", something we will be able to manually introduce in next JavaScript release.

Initial Weirdness


Funny enough, even if we reassign indexed properties, these will not be exposes in the loop, but if we set an extra index or property, this one will probably be exposed.

checkArgs(1, 2, 3);

function checkArgs(){
for(var i = 0; i < arguments.length; ++i)
arguments[i] = arguments[i];
// the extra index, manually assigned
arguments[3] = 4;
for(var key in arguments)
alert(key);
// will be 3 in FF and IE
// still nothing in Chrome
};

And as soon as we change the length property, this one could be exposed (FF, IE) or not (Chrome). So what we have so far?

Zero Stress for Extended Object.prototype Checks?


Since as I said arguments is always part of a function body, we could use this variable to know if some piece of code changed the Object prototype, rather than create a new object for each check.

function someGeniusExtendedObjectPrototype(){
for(var k in arguments)
return true;
return false;
};

Unfortunately, this is not worthy, because the empty inline Object instance creation performs better than a scope resolution for an undeclared variable as arguments is.

function someGeniusExtendedObjectPrototype(){
for(var k in {})return true;return false;
};
// that's it


An Instance With Not Exposed Indexes?


We could think to use arguments as a Function prototype in order to create objects ... wait a minute, NO! We could think to create a function that will directly make the magic argument public, rather than internal.

// global window scope
// where arguments has no
// meaning
// NOTE: do not use with Internet Explorer
function arguments(){
return arguments;
};

// let's test it
var a = arguments(1, 2, 3);

// loop ?
for(var k in a)
alert(k); // nothing!

// it "works"!

Unfortunately, the super innovative, secure, and efficient with advanced tab isolation and recovery browser Internet Explorer 8 crashes like a charm, so we need to rename the function into something less ambiguous for the magic JScript engine.

// use with Internet Explorer too
function args(){
return arguments;
};


A Magic Object Creation?

Using latest snippet, we could abuse about "for in" feature to create our favourite library ... maybe ...

function MyCoolCollection(){
// will be exposed in IE and FireFox
arguments.push = Array.prototype.push;
// will be exposed in FireFox
arguments.toString = Array.prototype.join;
return arguments;
};

var a = MyCoolCollection(1, 2, 3);

// nothing only in Chrome
for(var k in a)
alert(k);

// with this, IE will expose added index "3" and length
a.push(4);

alert(a); // 1,2,3,4

Nada, nien, nothing ... arguments is destined to be arguments, damn it!

arguments as prototype

Trying to discover more arguments freaking stuff, I used this magic variable as prototype:

(function(){"use strict";

// reusable in-scope constructor
function $(){};

// the magic ArgList function
window.ArgList = function ArgList(){

// assign arguments as prototype
$.prototype = arguments;

// return a new magic instance
return new $;
};
})();

var a = ArgList(1,2,3);

Everything seems to be OK except Internet Explorer does not expose properties until you manually discover them:

// after above snippet ...
Array.prototype.slice.call(a);
// ",," in Internet Explorer
// 1,2,3 in every other browser

Cool enough, we just discover a property and magic happens again:

a[1];
Array.prototype.slice.call(a);
// ",2," in Internet Explorer

Sometimes I forget we have to deal with a browser which logic is to bring internally even user comments:

var f = ( /* hi scope! */ function(){ /* hi bracket! */ } );
alert(f);
// will be exactly:
// ( /* hi scope! */ function(){ /* hi bracket! */ } )
// the magic of an efficient code parser

As summary, to use arguments as prototype the function should be modified for IE:

(function(){"use strict";
function $(){};
window.ArgList = function ArgList(){
$.prototype = arguments;
var o = new $, i = o.length;
while(i--)o[i]; // just access ...
return o;
};
})();


Firefox Specific Weirdness

arguments has some secret negative index in every Firefox (Spidermonkey) engine. For example, the index [-2] contains the length of the arguments object, but it does not perform faster than arguments.length, useless. The index [-3] contains the function itself:

function args(){
alert(arguments[-3] === arguments.callee);
alert(arguments[-2] === arguments.length);
};

In a valueOf operation, arguments[0] contains a sort of information, undefined, or "number", which could tell us if the object is going to be casted as a number:

var o = {
push:[].push,
length:0,
toString:[].join,
valueOf:function(){
return arguments[0] == "number" ? this.length : this.toString();
}
};

o.push(1, 2, 3);

alert(o); // 1,2,3
alert(o*1); // 3


Is That It?


I am sure every browser has some weird behavior with arguments variable so more than an inspiration for ArrayLike Objects, we cannot truly rely that much in this magic variable. What did you discover guys?

2 comments:

kangax said...

I wonder if it would be possible to access `arguments.callee` via `arguments[-3]` when in ES5-strict.

If so, it is probably a security hole which will need to be taken care of.

Andrea Giammarchi said...

Juryi, as you know I am against arguments.callee decision for ECMAScript.Next but I am sure -2 and -3 are for internal purpose only, exposed only if manually called (for this reason slower to "discover"). Accordingly, the day arguments.callee will disappear it does not make sense to retrieve it via -3 if the all goal is to get rid of it so I think it will not be usable while -2 for the lenght will be still there.