My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Tuesday, April 07, 2009

JavaScript Static Collection - A New Type Of Variable

Nowadays, I am working over a crazy manager between an XML with form information, another XML with custom rules to drive the XML with form information (DropDown onchange events, validation, and much more) and both to drive a runtime Ext.form.FormPanel with runtime components manipulated runtime via those two XML and over regular (x)HTML ... a runtime nightmare, but it is not the point.

While I was working over an Easy XPath Library for (x)HTML, trying to obtain best performances with the black sheep Internet Explorer, I ended up in an msdn page which explains the attributes HTML Element property.

Funny enough, that kind of property has truly a weird behavior, it is between VBScript and JavaScript. There are two ways to access its key/value pairs, the normal and logical one, plus the WTF!!! one. Example:

for(var
attrs = document.body.attributes,
len = attrs.length,
i = 0;
i < len;
++i
)
attrs(i) === attrs[i]; // true

The first though was: damn it, somebody poisoned my coffee again! (where the first time was when I read about Microsoft adopting WebKit as browser engine ...) but after few millisecond of brain delay, I realized that was brilliant!

The StaticCollection Type


Every Function in JavaScript has a length property, which tell us how many arguments that function accepts. In a completely dynamic language as JS is, this property has probably never been used, specially because every function has an ArrayObject arguments variable injected for each function call (but I guess would be interesting to use checks like: arguments.length > arguments.callee.length, for secret extra arguments to send ... anyway ... ).

Peculiarity of this property, we cannot overwrite it!
In few words, it is possible to create an Object via Function with immutable access over a collection, array, ArrayObject variable. This is the code:

var StaticCollection = (function(
name, // function name to assign
join // toString method from Array.prototype
){
// (C) WebReflection - Mit Style License
return function(){
var args = arguments, // trap sent arguments
length = args.length, // just a shortcut
i = 0, // used to assign values in order
callback; // Function to create and return
eval(
// create a function with a $name and $length arguments
// to block its length property
"callback = function ".concat(
name, "(",
new Array(length + 1).join(",$").substring(1),
// return first sent key from external arguments
"){return args[arguments[0]];}"
));
// assign in order properties via index ([0], [1], etc)
while(i < length)
callback[i] = args[i++];
// set the name for those browser where
// the function name is not present (mainly IE)
callback.name = name;
// set the useful toString method
callback.toString = join;
// return the FunctionObject
return callback;
};
})("StaticCollection", Array.prototype.join);

Due to fixed length behavior, the evaluation is absolutely necessary but not scary at all, since the string does not contain anything important, it is a simple wrap.
For those in Rhino or with some arguable rules about eval, here there is the intermediate step via Function:

...
i = 0,
callback= Function(
"args",
"return function ".concat(
name, "(", new Array(length + 1).join(",$").substring(1),
"){return args[arguments[0]];}"
)
).call(this, args);
...

which result is exactly the same but with a little bit of overhead caused by useless runtime Function execution.

StaticCollection Goals


  • one variable with static properties access plus indexed

  • indexed and called properties are comparable

  • indexes are mutable, perfectly suitable for relations between different objects/nodes/values

  • is the only quick and dirty way I know to make a collection immutable

  • something else, you'll find out :D



The StaticCollection Weirdness In Action


// Creation: same result
var sc1 = new StaticCollection("a", "b", "c"),
sc2 = StaticCollection("a", "b", "c"),
sc3 = StaticCollection.call(this, "a", "b", "c"),
sc4 = StaticCollection.apply(this, "abc".split(""))
;
/** [sc1, sc2, sc3, sc4].join("\n");
a,b,c
a,b,c
a,b,c
a,b,c
*/


// Properties Access
var sc = new StaticCollection("a", "b", "c")
sc(0); // "a"
sc[0]; // "a"
for(var key in sc){
key; // "a"
break;
};


// Immutable Properties: length + access via call
var sc = StaticCollection("a", "b", "c");
sc.length = 0;
sc.length; // 3
sc(1); // "b"
// sc(1) = 123; // error!


// Multiple access manifest
var sc = StaticCollection(456);
sc[0] === sc(0); // true
sc[0] = 123;
sc(0); // 456
sc[0]; // 123
for(var key in sc)
sc[key] !== sc(key); // true


// Type: function
typeof StaticCollection(1,2,3);


// Native toString: [object Function]
Object.prototype.toString.call(StaticCollection());


// Array Convertion:
var sc = StaticCollection(1,2,3);
sc.slice = Array.prototype.slice;
var a = sc.slice();
var b = Array.prototype.slice.call(
StaticCollection(4, 5, 6)
);


// Index Sort
var sc = StaticCollection(2, 1, 3);
sc; // 2, 1, 3
sc[0]; // 2
Array.prototype.sort.call(sc, function(a, b){
return a < b ? -1 : 1;
});
sc; // 1, 2, 3
sc[0]; // 1
sc(0); // 2


// Double indexOf
StaticCollectionSearch = function(value){
for(var
result = [-1, -1],
length = this.length,
i = 0;
i < length;
++i
){
if(this(i) === value){
result[0] = i;
break;
}
};
for(i in this){
if(/^\d+$/.test(i) && this[i] === value){
result[1] = i >> 0;
break;
}
};
return result;
};
var sc = StaticCollection(2, 1, 3);
Array.prototype.sort.call(sc, function(a, b){
return a < b ? -1 : 1;
});
StaticCollectionSearch.call(sc, 1); // 1, 0


// Array prototypes behaviour example
var sc = StaticCollection(1,2,3);
Array.prototype.shift.call(sc);
sc; // 2,3,
sc[0]; // 2
sc(0); // 1
sc.length; // 3


Crazy enough? The manually minified version is under 300 bytes, so let's start to play with ;)

/*WebReflection*/(function(n,j){this[n]=function(){var a=arguments,l=a.length,i=0,f;eval("f=function "+n+"("+new Array(l+1).join(",$").substring(1)+"){return a[arguments[0]]}");while(i<l)f[i]=a[i++];f.name=n;f.toString=j;return f}})("StaticCollection",Array.prototype.join);

Have fun!

9 comments:

Àl said...

Buf! What you do with JS is incredible. It's extremately malleable. JS always has another twirl from its few elements to create a new trick. Those newcomers that ask for new features should read this.

Jack said...

Woow, really brilliant :)

Andrea Giammarchi said...

Fun enough that browsers have different limits for function arguments :)

// IE6, IE7, IE8
32767

// FireFox
65535

// Chrome
129933

// Opera
65534

Brett said...

You da man! Looks similar to a "tuple" in Python... Small tip, as Crockford suggests in his book, you can usually use the slightly shorter "slice" instead of "substring" (though not if you are wary of negative values). Thanks for keeping JS ever interesting!

Andrea Giammarchi said...

Aboutthe tuple, I was thinking about JSON representation indeed, something like an array but with round brackets
(1,4,5,6,7)
immutable ... sounds cool, now I have to convince every browser vendor about this stuff :D

kangax said...

This "collection" is only "immutable" when you access members via a function call. All the numeric properties that a function is being extended with (0,1,2,...) can all still be modified.

If you need a function call to simulate immutable collection, you might as well just get rid of an unnecessary slow `eval`:


var Collection = (function(join){
return function() {
var args = arguments,
i = args.length;
function F(){
return args[arguments[0]]
}
F.toString = function() {
return String(join.call(args))
}
while (i--) { F[i] = args[i] }
return F;
}
})(Array.prototype.join);

Collection(1,2,3)(1); // 2

Did I miss something?

Andrea Giammarchi said...

simply the length, something kinda important for an index based collection type.

All the rest is a bit obvious, and still obviously you did not get the double access possibility ... am I wrong?

Private variables are not a news, a variable with double behavior and different way to be accessed is, as far as I know. This is the new type, the rest is not new at all, is it?

Andrea Giammarchi said...

P.S. "simply the length" means you need eval to obtain the length. I cannot believe you though so superficially about eval/Function choice ...

kangax said...

Ahha. I completely missed the `length` peculiarity. Function objects' `length` is indeed a ReadOnly property, so constructing a function is the only way to set it.