4

I have been playing with javascript arrays and I have run into, what I feel, are some inconsistencies, I hope someone can explain them for me.

Lets start with this:


var myArray = [1, 2, 3, 4, 5];
document.write("Length: " + myArray.length + "<br />");
for( var i in myArray){
   document.write( "myArray[" + i + "] = " + myArray[i] + "<br />");
}
document.write(myArray.join(", ") + "<br /><br />");
Length: 5
myArray[0] = 1
myArray[1] = 2
myArray[2] = 3
myArray[3] = 4
myArray[4] = 5
1, 2, 3, 4, 5

There is nothing special about this code, but I understand that a javascript array is an object, so properities may be add to the array, the way these properities are added to an array seems inconsistent to me.

Before continuing, let me note how string values are to be converted to number values in javascript.

  • Nonempty string -> Numeric value of string or NaN

  • Empty string -> 0

So since a javascript array is an object the following is legal:


myArray["someThing"] = "someThing";
myArray[""] = "Empty String";
myArray["4"] = "four";

for( var i in myArray){ document.write( "myArray[" + i + "] = " + myArray[i] + "<br />"); } document.write(myArray.join(", ") + "<br /><br />");

Length: 5
myArray[0] = 1
myArray[1] = 2
myArray[2] = 3
myArray[3] = 4
myArray[4] = four
myArray[someThing] = someThing
myArray[] = Empty String
1, 2, 3, 4, four

The output is unexpected.

The non empty string "4" is converted into its numeric value when setting the property myArray["4"], this seems right. However the empty string "" is not converted into its numeric value, 0, it is treated as an empty string. Also the non empty string "something" is not converted to its numeric value, NaN, it is treated as a string. So which is it? is the statement inside myArray[] in numeric or string context?

Also, why are the two, non numeric, properities of myArray not included in myArray.length and myArray.join(", ")?

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
ForYourOwnGood
  • 38,822
  • 5
  • 30
  • 27

5 Answers5

12

The keys of a JavaScript array are actually strings. For details and an implementation of a map type for arbitrary keys, check this answer.


To clarify and add to what Jason posted: JavaScript arrays are objects. Objects have properties. A property name is a string value. Therefore, array indices are converted to strings as well before anything more can happen. A property name P will be treated as an array index (ie the special array-magic will be invoked) if the following holds (ECMA-262, 15.4):

ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 2^32 − 1

That numeric indices will be converted to strings (and not the other way around) can be easily verified:

var array = [];
array[1] = 'foo';
array['1'] = 'bar';
array['+1'] = 'baz';
document.writeln(array[1]); // outputs bar

Also, its bad practice to iterate over an array's entries with a for..in loop - you might get unexpected results if someone messed with some prototypes (and it's not really fast, either). Use the standard for(var i= 0; i < array.length; ++i) instead.

Community
  • 1
  • 1
Christoph
  • 164,997
  • 36
  • 182
  • 240
  • We're both right. They're strings but the numerical indices get interpreted in a special way. – Jason S Jan 17 '09 at 18:45
  • @Jason: Nitpick: The standard says a property name P is treated specially if ToString(ToUint32(P)) is equal to P. The property name itself is a string value, so I stand by my statement – Christoph Jan 17 '09 at 18:49
  • Nitpick: my "We're both right" was meant to apply to your answer and mine. You are correct that my statement of "This isn't quite true" isn't quite true. :) – Jason S Jan 17 '09 at 18:58
  • @Jason: Ok. I have also added a small explanation (should be correct, or did I misrepresent anything?); and if everybody is right, we can all live happily ever after ;) – Christoph Jan 17 '09 at 18:59
  • looks good, you win ;) +1 for the foo/bar/baz example. (sympathy points always welcome...) – Jason S Jan 17 '09 at 19:21
  • @Jason: done - your answer and comments led to my extended answer, so you might as well get some of the credit... – Christoph Jan 17 '09 at 21:36
  • Just to be clear: I wrote I disagree, not that you are wrong and me right; I find the discussion interesting! :-) – PhiLho Jan 18 '09 at 20:28
  • @PhiLho: Yeah, the standard isn'T as clear as it could be; but I think my example is sufficient to prove whats going on... – Christoph Jan 18 '09 at 23:37
  • The standard is "normative", meaning it's written with Javascript implementers in mind, to unambiguously specify the language -- and only secondarily is it there to inform us poor old users. :( It would be nice if there were a companion guide for users which explained these subtleties in detail. – Jason S Jan 19 '09 at 14:12
2

(edit: the following is not quite right)

The keys of a JavaScript Object are actually strings. A Javascript Array by itself has numeric indices. If you store something with an index that can be interpreted as a nonnegative integer, it will try to do so. If you store something with an index that is not a nonnegative integer (e.g. it's alphanumeric, negative, or a floating-point number with a fractional piece), it will fail on the array-index store, and default to the Object (which is Array's base class) store, which then converts the argument to a string and stores by string index -- but these stored properties are not seen by the Array class and therefore are not visible to its methods/properties (length, join, slice, splice, push, pop, etc).

edit: the above is not quite right (as Christopher's foo/bar/baz example shows). The actual storage indices according to the ECMAscript spec are, in fact, strings, but if they are valid array indices (nonnegative integers) then the Array object's [[Put]] method, which is special, makes those particular values visible to the Array's "array-ish" methods.

Jason S
  • 184,598
  • 164
  • 608
  • 970
2

This is an answer to PhiLo's post. His benchmark is flawed because he uses different property names for the object version: He should have used i as well and not x.

If done correctly, eg like this:

var start, end, count = 1000000;

var obj = {},
    array = [];

start = new Date;
for(var i = count; i--; )
    array[i] = i;
end = new Date;
document.writeln(Number(end) - Number(start));

start = new Date;
for(var i = count; i--; )
    obj[i] = i;
end = new Date;
document.writeln(Number(end) - Number(start));

You'll see that the times will be very close. In FF3.0.5, the array version is even consistently slower (in Opera, it's the other way around).

Community
  • 1
  • 1
Christoph
  • 164,997
  • 36
  • 182
  • 240
  • Thanks for the remark and idea of additional tests. I wrote my answer as an edit of my message. Now, the important conclusion is your remark about looping on arrays, and the fact they are optimized for integer indices. The remainder is detail, no? :-) – PhiLho Jan 18 '09 at 13:35
0

I disagree with Christoph when he states "array indices are converted to strings".

First, I think it is implementation dependent... I suppose (good) implementers will optimize array access, there are some smart ways to do it.

Actually, I did a little test, and although it is as good as most micro-benchmarks (ie. not super-reliable), it is interesting:

result = ""
var x;

var trueArray = []
var startTime = new Date();
for (var i = 0; i < 100000; i++)
{
  x = "i" + i; // To do the same operations
  trueArray[i] = 1;
}
var endTime = new Date();
result += "With array: " + (endTime - startTime) + "\n";

var sArray = []
var startTime = new Date();
for (var i = 0; i < 100000; i++)
{
  x = "" + i;
  sArray[x] = 1;
}
var endTime = new Date();
result += "With s array: " + (endTime - startTime) + "\n";

var objArray = {}
var startTime = new Date();
for (var i = 0; i < 100000; i++)
{
  x = "i" + i;
  objArray[x] = 1;
}
var endTime = new Date();
result += "With object(i): " + (endTime - startTime) + "\n";

var sobjArray = {}
var startTime = new Date();
for (var i = 0; i < 100000; i++)
{
  x = "" + i;
  sobjArray[x] = 1;
}
var endTime = new Date();
result += "With s object: " + (endTime - startTime) + "\n";

var iobjArray = {}
var startTime = new Date();
for (var i = 0; i < 100000; i++)
{
  x = "" + i;
  iobjArray[i] = 1;
}
var endTime = new Date();
result += "With i object: " + (endTime - startTime) + "\n";


// Then display result

On IE6, I get: With array: 1453 With object: 3547
On FF 3.0, I get: With array: 83 With object: 226
On Safari 3.1, I get: With array: 140 With object: 313
On Opera 9.26, for some reason I don't get the result, but if I reduce to the tenth of number of loops, I get: With array: 47 With object: 516
Actually, I let Opera run while I type this, and finally got the result: With array: 281 With object: 166063...

So arrays are optimized! Which is fortunate...
Christoph's demonstration didn't impress me. My conclusion would be more that strings that can be interpreted as numbers are treated as such, which go along with the quoted formula...

So my interpretation of your results is that the array is behaving like a fast array with numerical indices when feed with these (with perhaps a behavior of associative array on sparse values, ie. some isolated big indices), but as an object, it still has the normal handling of properties. But these properties aren't handled in the array part, hence the result you got with join().

[EDIT] I added some loops, following Christoph's idea.
On FF3, I get: With array: 92 With s array: 93 With object(i): 243 With s object: 194 With i object: 125 (perfs vary between runs, but are roughly consistent).

I am not super-convinced of this integer -> string -> integer round-trip, not even that ECMA requests this sequence. The way I read it is: is the property is a string and can be interpreted as integer, then it is treated as such.

Of course, the only sure way to know would be to look at an implementation...

I notice with interest that plain objects getting an integer property or a property that can be transformed to an integer are somehow optimized. Perhaps because lot of JS programmers used plain objects as arrays, so implementers judged interesting to optimize this case.

PhiLho
  • 40,535
  • 6
  • 96
  • 134
  • Read my answer again: Numeric indices *are* treated specially; but before that, they'll be treated as regular property names, ie strings! so if you do something like array[1], it will go number->string (because its a property name) and then string->number again (because of array magic) – Christoph Jan 18 '09 at 10:56
  • And it's not implementation dependant: check ECMA-262! Only the implementation of arrays is unspecified, not their semantic properties! – Christoph Jan 18 '09 at 10:58
  • "I am not super-convinced of this integer -> string -> integer round-trip" - naturally, implementations can optimize this - but the standard requires that they behave as if they wouldn't – Christoph Jan 18 '09 at 14:26
  • The appropriate sections of ECMA-262: 15.4 (p. 100) and 15.4.5.1 (p. 109) – Christoph Jan 18 '09 at 14:27
  • pay special attention to 15.4.5.1, step 7 and following: the array magic happens AFTER the index was used as a regular property name - which means string! – Christoph Jan 18 '09 at 14:36
  • @Christoph: call me thick, but I read in 15.4.5.1: "Assume A is an Array object and P is a string." There is ambiguity as they don't address the case where P is an integer, but perhaps it is too obvious. Now, standard interpretation is delicate, see the W3C standards and how browsers interpret them! – PhiLho Jan 18 '09 at 19:31
  • @PhiLho: They don't address the case 'P is an integer' because it can't occur: The arguments to [[Put]] (the thing which will be called when you do `foo[???] = ???`) is a property name and a value; a property name is a string; there's nothing more that I can do than say it again and again ;) – Christoph Jan 18 '09 at 23:35
  • Christoph is correct. If you have questions about the spec, the mozilla.dev.tech.js-engine newsgroup is a good place to ask. – Jason S Jan 19 '09 at 14:09
0

Arrays, like everything else in JavaScript, are objects. Objects have been blessed with the dot notation to ease the burden on developers. Using your example

myArray["someThing"] = "someThing";

is the same as writing

myArray.someThing = "someThing";

In this case, you are adding a property onto the object instead of adding it into the array. The same goes with the empty string, although you cannot use the dot notation for an empty string...strange, huh?

In the case of "4", it can be coerced into an integer, and therefore is used as an index into the array.

Michael Deardeuff
  • 10,386
  • 5
  • 51
  • 74