Quiz series: Micro JavaScript / How _not_ to copy arrays
[Prototype | http://prototype.conio.net/] library has a wonderful function named $A. Btw, funny enough phenomena with naming convention used for global functions in Ajax libraries – if it is soooo global and useful then it deserves a
$
π Ok, $A is really useful, personally, I’d added several more
$$
to it.
Prototype library intensively used so-call parameter binding (or “curling”). I’ll discuss this in later post, but for now just imaging the following: if function receives less arguments then expecting, it returns function with same behavior but with shorter arguments list while part of parameters are “bound” to original argument values:
function mult() {
switch (arguments.length) {
case 0: return mult;
case 1:
var a = arguments[0];
var mult_by = function() {
return arguments.length ? a * arguments[0] : mult_by;
};
return mult_by;
default: return arguments[0] * arguments[1];
}
}
var a = mult( 19, 77 ); // 1463
var b = mult( 8, 9); // 72
var twice = mult(2); // mult with first parameter bound to 2
var c = twice(14.5) // 29
Hope you got the idea. So Prototype uses this technique intensively, but actual implementation is slightly different. Sure, it’s done in more generic manner. And to support this “generic manner” it’s necessary to clone original arguments of function to Array, modify it and further pass to “curled” nested function via Function.apply.
However, when I read the code for $A function I was shocked. It’s hard to imaging more inefficient implementation:
var $A = Array.from = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) {
return iterable.toArray();
} else {
var results = [];
for (var i = 0; i < iterable.length; i++)
results.push(iterable[i]);
return results;
}
}
First line is obvious – if we pass something that do not evaluates to true (null, undefined, empty string (?), zero(?), NaN(?) ) we just return empty array. Next is obvious too: if object supports own method to get array we use it. The broken code is actually “manual” copying of array-like objects.
“Array-like” is term used to describe JavaScript object that walks and quacks like Array: they have integer-indexed properties and a length property that describes number of indexed properties.
If you’ve ever been on other side of JavaScript, i.e. you’ve created own host objects for Rhino or SpiderMonkey interpreter, you may share my frustration. See, any “array-like” has length property, so we (or they, Prototypers) know size of result in advance. We can use Array(size) constructor to allocate necessary memory for result only once and then just set every element by index, instead of pushing elements one by one and resizing array with every operation. Do I need to clarify what is more efficient? I bet no…
But the inefficiency doesn’t end here. The way original iterable is iterated is an example of bad coding as well. Think, how many times iterable.length is calculated in loop? One? You are kidding, this is not binary code compiled by C compiler! iterable.length is calculated iterable.length times. Plain dumb. And final performance hint: iteration works faster for Aray if it is done in reverse order. Don’t ask me why – it simply works this way. You can find plenty of related tests with Google. And for logic of this code block, iteration order doesn’t matter. So, final result is:
var $A = Array.from = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) {
return iterable.toArray();
} else {
var cnt = iterable.length;
var results = new Array(cnt);
for (var i = cnt - 1; i >= 0; i--)
results[i] = iterable[i];
return results;
}
}
Readable/Explicit/Simple? Yes. Yes. Have the same size? Yes. And far more efficient.
Oh, the quiz question. Just for academic interest, how many chars are necessary to copy “array-like” object to JavaScript Array? Efficiency aside π Given:
var a = ;
var b = <expression>;
alert(“Size: ” + b.length + “\n” + b.join(“\n”));
Your turn!
And keep them coming!
Thanks for promotion! π
To those who whant to participate: note, I know only good enough (or "short enough" for this matter) solution. You have a great chance to come up with something outstanding that beats my own answer! But I promise to provide my answer "as is" even in this case π
Valery
I started off with the simplest way:<br/><br/>var a = ;<br/>var size=a.length;<br/>var b=new Array(size);<br/>for (var i=0;i<size;i++)<br/> b[i]=a[i];alert(b);<br/><br/>But then I figured -- why bother? "a" is basically an Array, but just without the label of an Array... I can treat it as one because the keys to the values (the values being A, B, C) are numbers -- 0, 1, 2 -- just like in an Array. So, this would work just fine:<br/><br/>alert(a[0])<br/><br/>Just like it's an Array. What I'm missing is the functions... If I need the join() function I plug it in:<br/><br/>a.join = Array.prototype.join<br/><br/>and voila! I can run join() on the object a:<br/><br/>alert(a.join());<br/><br/>will return "A, B, C". This will work for any Array object... π I doubt this is what you were going for, but it's another way to show how versatile this nifty JavaScript is :-)<br/><br/><br/>Cheers,<br/>Alon
You are cheating ;)))
The actual question (right above code) states that you must convert array-like to true Array object.
If you'd like, I can add more conditions to met:
b instanceof Array === true;
b.constructor === Array;
But I guess it's obviuos what is really necessary π
Valery
Two solutions<br/>One straight forward(39 characters)<br/>b=[];for(i=0;b.push(a[i+])<a.length;);<br/><br/>And one using regexp (54 characters), not very readable but that was hardly the goal :)<br/>for(b=[];b.push(/./.exec(a[b.length])[0])<a.length;);<br/><br/>The regexp could have been much shorter, but couldn't find a way to return all matches at once.<br/><br/>(another alternative for(b=[],i=0;b.push(/./.exec(a[i+])[0])<a.length;); is one character shorter, but much more boring)
The last sentence should be :<br/>another alternative for(b=[],i=0;b.push(/.*/.exec(a[i++])[0])<a.length;); is one character shorter, but much more boring<br/><br/>Hopefully it will come through this time
One more try. <br/>It seems it interprets <a.length as the start tag of an HTML element.<br/><br/>Two solutions<br/>One straight forward(39 characters)<br/>b=[];for(i=0;b.push(a[i+])<a.length;);<br/><br/>And one using regexp (54 characters), not very readable but that was hardly the goal :)<br/>for(b=[];b.push(/./.exec(a[b.length])[0])<a.length;);<br/><br/>The regexp could have been much shorter, but couldn't find a way to return all matches at once.<br/><br/>(another alternative for(b=[],i=0;b.push(/./.exec(a[i+])[0])<a.length;); is one character shorter, but much more boring)
Thanks for participating!
The stuff with RegExp just crack my head -- never ever guess RegExp can be applicable π
If Craig read this, I'd like to ask him to assign at least 10 points for you -- you win in "The most surprising solution" nomination!
But, actually, I'm not fan of imperative language constructs, so you get first hint -- "for" loop is unnecessary.
Valery
Flattern to primitive form??? To string???
Hint: array-like... like an array... almost the same as an array... :))
Valery
I've also had a quick look at changing the prototype, inheritFrom and/or constructor methods on the a object, but they all failed, so I have officially given up π
b = [];for(var i in a)b.push(a[i]);
I racked my brains over this couldnt get Object literals to be type converted to String,Array neither implicitly or using other hacks.
Keep it coming..
Believe me or not you repeat Eddy's solution again ;))
Considering shift between time zones you both leave, we can even assume that you provided same answer at the same time π
However, shortes expression exists (without loop).
Valery
Does it make any sense for you? π
Valery
b=[];b=Array.slice(a);
The idea behind this is that many array methods can be applied to all objects which look like arrays, in other words with a length property.
It fails though when one tries to apply the method directly on the object, thus the above solution should be used.
for IE 6.0 and Javascript<1.6
Judging from the doco it needs a length property.It also seems the value needs to match the number of properties available for that object(a bit non generic). Nevertheless a good insight
You puzzling me on your own whith question who wins π
1. Eddy's solution is correct and really short (however, it's JS 1.6)
Array.slice(arrayLike);
2. Pran's solution is correct and interoperable
Array.prototype.slice.call(arrayLike);
I guess Craig have enough bonus points for you both π
The latest question: how to shorten Array.prototype.slice?
Valery
Both have points - good job!!
Ok, here is an answer for quiz question:<br/>1. JS version < 1.6: var b = [].slice.call(a) // expression is 16 characters<br/>2. JS version 1.6: var b = Array.slice(a) // expression is 14 characters<br/><br/>The second part of answer was submitted by Eddy De Clercq. Strictly, first part is almost the same that Pran Bhas suggested:<br/><br/>Array.prototype.slice.call(a);<br/><br/>So, Craig, these guys both deserve points π <br/><br/>Seems, that it is really hard to get prototype-based OOP right π