Tuesday, April 7, 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!

Friday, April 3, 2009

A fast Array slice for every browser

It is an extremely common task and one of the most used prototype in libraries and applications: Array.prototype.slice
The peculiarity of this prototype is to create an Array from an Array like Object such arguments, HTMLCollection, other kind of lists as jQuery results.

Every browser, except Internet Explorer, allows direct calls via native prototype obtaining, obviously, best performances.

In IE, we have different ways to make this task possible, and these ways are mainly classic loops, or the isArray check to know when it is possible to perform the native call or not.


isArray = (function(toString){
return function(obj){
return toString.call(obj) === "[object Array]";
};
})(Object.prototype.toString);

Even using a closure, above function could slow down performances, specially in those libraries where Array conversions are performed almost everywhere.

As we all know, Internet Explorer behaves weird "sometimes", and one of the weirdest things is that native Objects are not instanceof Object.

In few words, instead of call a function to define if native slice could be applied, all we need to do is to check if the generic object is instanceof Object.
In this way native Arrays, arguments, everything with a length user defined, will be passed via native Array.prototype.slice, while for native objects, the good old loop will do the dirty job.

A better general purpose slice

$slice = (function(slice){
// WebReflection - Mit Style License
try {
// Chrome, FireFox, Opera, Safari, WebKit
slice.call(document.childNodes);
var $slice = slice;
} catch(e) {
// Internet Epxlorer
var $slice = function(begin, end){
// false with native objects/collections
// suitable for Array like Object (e.g arguments, jQuery, etc ...)
if(this instanceof Object)
return slice.call(this, begin || 0, end || this.length);
// ... every other case ...
for(var i = begin || 0, length = end || this.length, len = 0, result = []; i < length; ++i)
result[len++] = this[i];
return result;
};
};
return $slice;
})(Array.prototype.slice);

Simple, isn't it? And here a test case:
(function(){
try {

// HTMLCollection [object, ...]
$slice.call(document.getElementsByTagName("*"));

// arguments [3,2,1]
$slice.call(arguments);

// Array [1,2,3]
$slice.call([1,2,3]);

} catch(e) {

// should never happen
alert(e.message);
};
})(3,2,1);

where if nothing happen, simply means $slice worked without problems.

Side effects? With sandbox variables (iframes) IE will always use the loop, unless we do not bring its native Object constructor into the function (or the function into the sandbox)

Wednesday, April 1, 2009

TaskSpeed: DOM VS Libraries

I tried to create an essential library to perform a manual implementation of the TaskSpeed test suite and results surprised me.

All these web 2.0 libraries are extremely useful, cool, time-saving, whatever you want, but not a single one can compete with manual tasks implementation, specially when we are talking about the most common browser which at the same time is the slowest ever: Internet Explorer!


Final Results for an Dual Core under Windows XP SP3


FireFox 3.0.8
------------------------
library | ms
------------------------
DOM 383
plugd-a (Dojo) 734
Dojo 1.3.0 742
Dojo 1.2.3 1008
MooTools 1.2.1 1252
jQuery 1.3.2 1935
jQuery 1.2.6 3518


Safari 4 Beta (528.16)
------------------------
library | ms
------------------------
DOM 118
plugd-a (Dojo) 170
Dojo 1.3.0 186
Dojo 1.2.3 342
MooTools 1.2.1 347
jQuery 1.3.2 602
jQuery 1.2.6 1025


Opera 10.00 alpha
------------------------
library | ms
------------------------
DOM 111
Dojo 1.3.0 251
plugd-a (Dojo) 374
MooTools 1.2.1 688
Dojo 1.2.3 938
jQuery 1.3.2 1329
jQuery 1.2.6 2327


Internet Explorer 6
------------------------
library | ms
------------------------
DOM 3966
Dojo 1.2.3 22750
plugd-a (Dojo) 24189
Dojo 1.3.0 24249
jQuery 1.3.2 44314
MooTools 1.2.1 69346
jQuery 1.2.6 104407


Internet Explorer 7
------------------------
library | ms
------------------------
DOM 612
Dojo 1.2.3 3641
plugd-a (Dojo) 3748
Dojo 1.3.0 3861
jQuery 1.3.2 5031
MooTools 1.2.1 7658
jQuery 1.2.6 10468


Internet Explorer 8
------------------------
library | ms
------------------------
DOM 499
Dojo 1.3.0 2327
plugd-a (Dojo) 2455
Dojo 1.2.3 2922
jQuery 1.3.2 3170
MooTools 1.2.1 5876
jQuery 1.2.6 7049



The essential library


var $ = {
// essential stuff for TaskSpeed test by WebReflection
// Mit Style License
addEventListener:document.addEventListener?
function(name, callback, bool){
this.addEventListener(name, callback, bool);
}:
function(name, callback, bool){
this.attachEvent("on" + name, callback);
}
,
getSimple:function(selector){
for(var
split = selector.split("."),
result = [],
re = new RegExp("\\b" + split[1] + "\\b"),
list = this.getElementsByTagName(split[0] || "*"),
length = list.length,
i = 0,
j = 0,
node;
i < length; ++i
){
node = list[i];
if(re.test(node.className))
result[j++] = node;
};
return result;
},
indexOf:Array.prototype.indexOf || function(value, i){
for(var
length = this.length,
i = i < 0 ? i + length < 0 ? 0 : i + length : i || 0;
i < length && this[i] !== value;
++i
);
return length <= i ? -1 : i;
},
removeEventListener:document.removeEventListener?
function(name, callback, bool){
this.removeEventListener(name, callback, bool);
}:
function(name, callback, bool){
this.detachEvent("on" + name, callback);
}
,
slice:Array.prototype.slice
};


The test suite

// TaskSpeed, the DOM way by WebReflection
window.tests = {

"make": function(){
for(var
body = document.body,
ul = document.createElement("ul"),
li = document.createElement("li"),
i = 0,
fromcode;
i < 250; ++i
){
fromcode = ul.cloneNode(true);
fromcode.id = "setid" + i;
fromcode.className = "fromcode";
fromcode.appendChild(li.cloneNode(true)).appendChild(document.createTextNode("one"));
fromcode.appendChild(li.cloneNode(true)).appendChild(document.createTextNode("two"));
fromcode.appendChild(li.cloneNode(true)).appendChild(document.createTextNode("three"));
body.appendChild(fromcode);
};
return $.getSimple.call(body, "ul.fromcode").length;
},

"indexof" : function(){
for(var body = document.body, index = -1, i = 0; i < 20; ++i)
index = $.indexOf.call(body.getElementsByTagName("ul"), document.getElementById("setid150"));
return index;
},

"bind" : function(){
for(var callback = function(){}, li = document.body.getElementsByTagName("li"), length = li.length, i = 0, total = 0, node; i < length; ++i){
node = li[i];
if(node.parentNode.nodeName == "UL"){
++total;
$.addEventListener.call(node, "click", callback, false);
};
};
return total;
},

"attr" : function(){
for(var result = [], ul = document.body.getElementsByTagName("ul"), length = ul.length, i = 0; i < length; ++i)
result[i] = ul[i].id;
return result.length;
},

"bindattr" : function(){
for(var callback = function(){}, li = document.body.getElementsByTagName("li"), length = li.length, i = 0, total = 0, node; i < length; ++i){
node = li[i];
if(node.parentNode.nodeName == "UL"){
++total;
$.addEventListener.call(node, "mouseover", callback, false);
node.setAttribute("rel", "touched");
$.removeEventListener.call(node, "mouseover", callback, false);
};
};
return total;
},

"table": function(){
for(var
body = document.body,
table = document.createElement("table").appendChild(document.createElement("tbody")).parentNode,
tr = document.createElement("tr"),
td = document.createElement("td"),
length = 40,
i = 0,
madetable,
cell;
i < length; ++i
){
madetable = table.cloneNode(true);
madetable.className = "madetable";
cell = body.appendChild(madetable).firstChild.appendChild(tr.cloneNode(true)).appendChild(td.cloneNode(true));
cell.appendChild(document.createTextNode("first"));
cell.parentNode.insertBefore(td.cloneNode(true), cell);
};
tr = body.getElementsByTagName("tr");
length = tr.length;
i = 0;
for(var total = 0; i < length; ++i)
total += tr[i].getElementsByTagName("td").length;
return total;
},

"addanchor" : function(){
var a = document.createElement("a");
a.setAttribute("href", "http://example.com");
a.appendChild(document.createTextNode("link"));
for(var ul = $.getSimple.call(document.body, "ul.fromcode"), length = ul.length, i = 0, total = 0, childNodes, j, len, node; i < length; ++i){
childNodes = ul[i].childNodes;
j = 0;
len = childNodes.length;
while(j < len){
node = childNodes[j];
if(node.nodeName === "LI"){
++total;
node.appendChild(a.cloneNode(true));
};
++j;
};
};
return total;
},

"append": function(){
for(var div = document.createElement("div"), body = document.body, i = 0, node; i < 500; ++i){
node = div.cloneNode(true);
node.setAttribute("rel", "foo2");
body.appendChild(node);
};
for(var div = body.getElementsByTagName("div"), length = div.length, i = 0, total = 0; i < length; ++i)
total += div[i].getAttribute("rel") === "foo2";
return total;
},

"addclass-odd" : function(){
for(var div = document.body.getElementsByTagName("div"), length = div.length, i = 0, total = 0; i < length; ++i)
total += i % 2 ? !!(div[i].className += " added odd") : !(div[i].className += " added");
return total;
},

"style" : function(){
for(var div = $.getSimple.call(document.body, "div.added"), length = div.length, i = 0, style; i < length; ++i){
style = div[i].style;
style.backgroundColor = "#ededed";
style.color = "#fff";
};
return length;
},

"removeclass" : function(){
for(var re = /\s*\badded\b/g, div = $.getSimple.call(document.body, "div.added"), length = div.length, i = 0, node; i < length; ++i){
node = div[i];
node.className = node.className.replace(re, "");
};
return length;
},

"sethtml": function(){
for(var div = document.body.getElementsByTagName("div"), length = div.length, i = 0; i < length; ++i)
div.innerHTML = "<p>new content</p>";
return div.length;
},

"insertbefore" : function(){
for(var p = document.createElement("p"), ul = $.getSimple.call(document.body, "ul.fromcode"), length = ul.length, i = 0, total = 0; i < length; ++i){
for(var a = ul[i].getElementsByTagName("a"), len = a.length, j = 0, node; j < len; ++j){
++total;
node = a[j];
node.parentNode.insertBefore(p.cloneNode(true).appendChild(document.createTextNode("A Link")).parentNode, node);
};
};
return total;
},

"insertafter" : function(){
for(var p = document.createElement("p"), ul = $.getSimple.call(document.body, "ul.fromcode"), length = ul.length, i = 0, total = 0; i < length; ++i){
for(var a = ul[i].getElementsByTagName("a"), len = a.length, j = 0, node; j < len; ++j){
++total;
node = a[j];
node.parentNode.appendChild(p.cloneNode(true).appendChild(document.createTextNode("After Link")).parentNode);
};
};
return total;
},

"destroy": function(){
var result = $.getSimple.call(document.body, "ul.fromcode"),
length = result.length,
i = 0,
node;
while(i < length){
node = result[i++];
node.parentNode.removeChild(node);
};
return length;
},

"finale": function(){
var body = document.body;
while(body.firstChild)
body.removeChild(body.firstChild);
return body.getElementsByTagName("*").length;
}

};


Conclusion

I do not want to spend a word about these results, but I guess we have to think about them.