views:

413

answers:

6

Looking through the dom.js source from the Closure library I found this (in goog.dom.getElementsByTagNameAndClass_):

if (opt_class) {
var arrayLike = {};
var len = 0;
for (var i = 0, el; el = els[i]; i++) {
  var className = el.className;
  // Check if className has a split function since SVG className does not.
  if (typeof className.split == 'function' &&
      goog.array.contains(className.split(' '), opt_class)) {
    arrayLike[len++] = el;
  }
}
arrayLike.length = len;
return arrayLike;
}

What would be the benefit of doing this over a regular array?

A: 

As far as I know, there is no benefit because there is actually no difference whatsoever between this and a "regular array" except for the creation syntax - all Javascript objects are associative arrays.

Michael Borgwardt
Not exactly. Objects/associative arrays don't have a `length` property, which is why the Closure library is adding it in this case (e.g.: `console.log({}.length, [].length)` => "undefined 0").
Justin Johnson
Also, arrays ([]) have a wider set of methods (see Array.prototype).
Nickolay
+2  A: 

In this case, my guess would be that arrayLike[len++] = el is an optimization over actualArray.push(el). However, after doing a simple benchmark (code provided below results), it appears that this method is actually slower than using a standard array using the push method as well as with same construction technique.

Results (from OS X 10.5.8, FF 3.5.6) *:

push construction:        199ms (fastest)
indexed construction:     209ms
associative construction: 258ms (slowest)

In conclusion, why Closure is using an associative array in this case is beyond me. There may likely be a reason (for instance, this technique may perform better in Chrome, or less dubiously, this technique may perform better in future releases of JavaScript engines in general), but I don't see a good reason in this case.

* A mean was not provided because the times varied from test run to test run, but consistently resulted in the same order. If you're interested, you can carry it out on your own.

Benchmark code:

var MAX = 100000, i = 0, 
    a1 = {}, a2 = [], a3 = [],
    value = "";

for ( i=0; i<1024; ++i ) {
    value += "a";
}

console.time("associative construction");
for ( i=0; i<MAX; ++i ) {
    a1[i] = value;
}
a1.length = i;
console.timeEnd("associative construction");

console.time("push construction");
for ( i=0; i<MAX; ++i ) {
    a2.push(value);
}
console.timeEnd("push construction");

console.time("indexed construction");
for ( i=0; i<MAX; ++i ) {
    a3[i] = value;
}
console.timeEnd("indexed construction");

The size and type of value is insignificant to the test as JavaScript uses copy-on-write. A large (1kb) value was used for the purpose of convincing those readers who are not familiar with this feature of JavaScript.

Justin Johnson
Whenever doing a benchmark, remember to benchmark against IE 6. A lot of weird performance hacks you see may be there to work around some horrible performance characteristics of IE 6, while providing little if any benefit on other browsers.
Brian Campbell
Indeed, I am aware of that. But, like I said, I'm on a Mac at the moment and unable to test on a full range of browsers.
Justin Johnson
Exactly, Brian. And it would make sense to favor IE on performance due to the browser market share (there are a lot of haters out there, but facts are facts). Additionally, be sure to run this test with Google's V8 engine.
Josh Stodola
+1  A: 

The author of the code used empty JavaScript object as a basis of a array like object, i.e. the one that can be accessed by index and has a length property.

There could be two reasons that I could think of:

  1. memory use - if array is backed by implementation that allocates n elements, when it reaches it's limits it grows by some factor to increase it's capacity, and thusly wastes capacity - length of memory
  2. cpu time - implementor choose insertion speed over random access speed -- a return from this method is more likely to be iterated over sequentially than random accessed, and resizing the array in insertion causes allocation-copy-deallocation which has a cpu penalty

I'm betting that similar code would be found in other JavaScript libraries, and that it's result of benchmarking and finding the best to fit solution across different browsers.

edited after comment by Justin

Upon further googling it appears that array-like objects are common among JavaScript developers: checkout JavaScript: the definitive guide by David Flanagan, it has a whole sub-chapter on Array-like objects. Also these guys mention them.

No mention of why should one prefer array-like vs array object. This could be a good SO question.

So a third option could be the key: compliance with norms of JavaScript API's.

Zoran Regvart
Capacity isn't necessarily growing with each insert. It is a common optimization to grow the capacity by a value greater than 1 whenever a container's capacity is reached. I haven't looked at the actual engine's code, but I'd be willing to put money on that optimization being present. As for CPU time, my benchmark shows the opposite, granted that it is only in FF 3.5.6.
Justin Johnson
You are of course, right, but I did not (mean to) imply that capacity would increase with every insertion...
Zoran Regvart
A: 

I don't see any difference since alert(typeof []); returns "object". Any time I see something like this (especially from the Goog), I have to assume it is for a boost in performance.

It is essentially returining an array without all of the inherited functions such as push and pop.

Josh Stodola
I thought the same too. Turns out we were both wrong. See my benchmark
Justin Johnson
Actually, last time I checked, Google Closure was not optimized for performance at all, and looked more like a legacy code.
kangax
A: 
goog.dom.getElementsByTagNameAndClass_

You are dealing with html collections. An arrayLike object is a snapshot of a node list, rather than the 'live' collection object. It makes it as easy as an indexed array to work with, and less likely to cause complications if you create or delete nodes while looping through its members.

kennebec
A: 

I think this example creates an array-like object instead of a real array, because other DOM methods also return array-like objects (NodeList).

Consistently using "array-likes" in the API forces the developer to avoid using the array-specific methods (using goog.array instead), so there are fewer gotchas when someone later decides to change a getElementsByTagNameAndClass call to, say, getElementByTagName.

Nickolay