4

As we already know, one of the differences between an array and object is:

"If you want to supply specific keys, the only choice is an object. If you don't care about the keys, an array it is" (Read more here)

Besides, according to MDN's documentation:

Arrays cannot use strings as element indexes (as in an associative array) but must use integers

However, I was surprised that:

> var array = ["hello"]; // Key is numeric index
> array["hi"] = "weird"; // Key is string
content structure looks like:  ["hello", hi: "weird"]

The content structure of the array looks weird. Even more, when I check the type of array it returns true

Array.isArray(array) // true

Questions:

  1. Why have this behavior? This seems inconsistent, right?
  2. What is the data structure actually storing behind the scene: as an array or something like an object, hash table, linked list?
  3. Does this behavior depend on a specific JavaScript engine (V8, spidermonkey, etc..) or not?
  4. Should I use arrays like this (keys are both numeric index and string) over normal objects?
shaedrich
  • 5,457
  • 3
  • 26
  • 42
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
  • array in javascript is an object – Chris Li Apr 10 '21 at 01:58
  • How about MDN docs say that *"Arrays cannot use strings as element indexes (as in an associative array) but must use integers"* – Nguyễn Văn Phong Apr 10 '21 at 01:58
  • 2
    It's possible to create arbitrary properties on array objects, but you should absolutely not do this. [They are not associative arrays](https://andrewdupont.net/2006/05/18/javascript-associative-arrays-considered-harmful/). – Bergi Apr 10 '21 at 02:03
  • @NguyễnVănPhong what they said is you can only access element from array list using integer, using non integer will access the array's object property, its the next line of what you quoted – Chris Li Apr 10 '21 at 02:04
  • @NguyễnVănPhong I'm a bit late to the punch with my answer, but I thoroughly answered each of your questions, provided additional context before my answers, and I also provided credible resources to support my answers and provide additional research. Please let me know if I missed anything :) – Brandon McConnell Apr 15 '21 at 17:25
  • 1
    My TL;DR answer would be… (1) On the surface, it does seem inconsistent, but it's actually very consistent when you take a deeper look. (2) the array is an array, even behind the scenes, but arrays in JS are a type of object and thus can be assigned custom properties in addition to array elements. (3) Nope! This is just pure JavaScript. (4) If you require keys to be numeric and alphanumeric, use an object. HOWEVER, if you would like to use an array, it's perfectly okay to add custom properties to that array as well. Just keep in mind they are only properties of the object, not array elements. – Brandon McConnell Apr 15 '21 at 17:33
  • 1
    @NguyễnVănPhong Also, I didn't include this in my answer, but if you require keys, and you also want to maintain the insertion order of your keys (since JS objects cannot reliably retain their property order), consider using a Map object-type instead. **Further research:** [Map() to the rescue; adding order to Object properties](https://www.jstips.co/en/javascript/map-to-the-rescue-adding-order-to-object-properties/) – Brandon McConnell Apr 15 '21 at 17:38
  • Hi @Kevin B. Thanks for your review. But obviously, my question has more aspects that need to be discussed. While the old question and answers don't resolve my concerns. The old question asked `"How"`, while my question is `"Why"` including many aspects that need to be discussed. So pls kindly help me check once again. Thanks, sir – Nguyễn Văn Phong Apr 20 '21 at 09:37
  • Unfortunately, I can't find the useful answer from them. Especially, for 4 questions. I think you should reopen instead @Kenvin B – Nguyễn Văn Phong Apr 20 '21 at 14:18
  • 3
    @NguyễnVănPhong By that metric, i also see no useful answers here. ¯\\_(ツ)_/¯ More words doesn't make the question or answer more useful. The TLDR of everything here is arrays are objects, but should never be used as objects. The "why" is because arrays are objects. Nothing else is useful or programming related. – Kevin B Apr 20 '21 at 14:25
  • This question is being discussed on [Meta](https://meta.stackoverflow.com/questions/406976). – cigien Apr 21 '21 at 13:40
  • This question has most definitely been asked on Stack Overflow many times over in the past 12 years. – Peter Mortensen Apr 22 '21 at 11:14
  • 1
    To be honest, I get the useful answer from @VLAZ 's summary, research over the other ones. – Nguyễn Văn Phong Apr 22 '21 at 12:35

7 Answers7

11

An array in JavaScript is not a separate type, but a subclass of object (an instance of the class Array, in fact). The array (and list) semantics are layered on top of the behavior that you get automatically with every object in JavaScript. JavaScript objects are actually instances of the associative array abstract data type, also known as a dictionary, map, table, etc., and as such can have arbitrarily-named properties.

The rest of the section of the MDN docs you quoted is important:

Setting or accessing via non-integers using bracket notation (or dot notation) will not set or retrieve an element from the array list itself, but will set or access a variable associated with that array's object property collection. The array's object properties and list of array elements are separate, and the array's traversal and mutation operations cannot be applied to these named properties.

So sure, you can always set arbitrary properties on an array, because it's an object. When you do this, you might, depending on implementation, get a weird-looking result when you display the array in the console. But if you iterate over the array using the standard mechanisms (the for...of loop or .forEach method), those properties are not included:

> let array = ["hello"] 
> array["hi"] = "weird"
> for (const item of array) { console.log(item) }
hello
> array.forEach( (item) => console.log(item) )
hello

The MDN statement that "The array's object properties and list of array elements are separate" is slightly exaggerated; the indexed elements of an array are just ordinary object properties whose keys happen to be numbers. So if you iterate over all of the properties with for..in, then the numeric ones will show up along with the others. But as is well-documented, for..in ignores the Array-ness of arrays and should not be used if you're looking for arraylike behavior. Here's another page from MDN that talks about that:

Array iteration and for...in

Note: for...in should not be used to iterate over an Array where the index order is important. Array indexes are just enumerable properties with integer names and are otherwise identical to general object properties. There is no guarantee that for...in will return the indexes in any particular order. The for...in loop statement will return all enumerable properties, including those with non–integer names and those that are inherited.

Because the order of iteration is implementation-dependent, iterating over an array may not visit elements in a consistent order. Therefore, it is better to use a for loop with a numeric index (or Array.prototype.forEach() or the for...of loop) when iterating over arrays where the order of access is important.

So to answer your questions:

  1. Why have this behavior? This seems inconsistent, right?

"Why" questions are always difficult to answer, but fundamentally, the answer in this case seems to be that what you are seeing is not an intentional behavior. As far as we can tell, Brendan Eich and the subsequent designers of JavaScript didn't set out to make an array type that also allows non-numeric keys; instead, they elected not to make an array type at all. In JavaScript, there are only eight types: Null, Undefined, Boolean, Number, BigInt, String, Symbol, and Object. Array didn't make the cut — it's not a separate type, but just a class of object. In fact, you could remove arrays from the core language and implement them entirely in JavaScript itself (although you'd lose the convenience of the [...] literal syntax).

So that's why: JavaScript arrays aren't their own type, but just a class of object; all objects in JavaScript are associative arrays; therefore, you can use arrays as associative arrays.

  1. What is the data structure actually storing behind the scene: as an array or something like object, hashtable, linkedlist?

The ECMAScript specification requires objects to function as associative arrays, but does not dictate the implementation; you can assume that they're hash tables without losing any generality. But since the Array class is typically part of the core language implementation rather than pure JavaScript runtime code, I would not be surprised to find that it included special optimizations beyond the generic-object property-handling code for handling the numerically-indexed values more efficiently. As long as the semantics are the same, it doesn't matter.

  1. Whether this behavior depends on a specific JavaScript Engine (V8, SpiderMonkey, etc..) or not?

Different engines might change how Array values are displayed/serialized, and in particular whether such string representations include the non-numeric properties. But the underlying behavior of being able to store arbitrary key–value pairs is a natural side-effect of the language's design and should be universal across all implementations compliant with the ECMAScript specification.

  1. Should I use array like this(keys are both numeric index and string) over normal object?

Well, again, an array is a normal object. But for maximum readability and minimum surprise to those reading your code, I would not recommend using the same object as both a regular and associative array. You could implement a new class that has array-like behavior, maybe even inherit from the Array prototype, but it's usually going to be better to keep those two data structure types separate.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Mark Reed
  • 91,912
  • 16
  • 138
  • 175
  • 2
    "*although you'd lose the conveniences of the for...of loop*" not really. A `for..of` intrinsically fetches the iterator of the object, so if you implement your "fake array" to have an iterator that supplies only positive integer-indexed values, you'd get the same behaviour as `for..of` over a normal array. Because that's basically what happens anyway `for (const item of something)` will iterate over `something[Symbol.iterator]()` and for arrays that is an alias for the `arr.values()` iterator. – VLAZ Apr 21 '21 at 07:44
  • 1
    [Quick demo of such an implementation](https://jsbin.com/vazusiw/1/edit?js,console) – VLAZ Apr 21 '21 at 07:51
  • I like this implementation `*[Symbol.iterator]()` and `if (Number.isInteger(key) && key >= 0) yield entry[1];` Your idea seems like the behind the scenes of `associative array javascript`@VLAZ – Nguyễn Văn Phong Apr 21 '21 at 07:57
  • 3
    @NguyễnVănPhong actually, a small bug - it should also check `key < this.length`. But yeah. It's to illustrate a point. The only thing really missing from a custom implementation would be the array literal syntax `[1, 2, 3]`. Other than that, everything else is standard object behaviour. The array methods can be very easily re-created, if needed. Although, they don't need to - all of them are intentionally generic, so they can be applied to array-like objects (ones that have positive integers as keys and a `length` property). – VLAZ Apr 21 '21 at 08:24
  • Good point, @VLAZ. Updated to remove that caveat.Thanks! – Mark Reed Apr 21 '21 at 17:14
3

This stumped me recently as well! It seemed to me that if JavaScript allows something like ['a', b: 'c'], it must support associative arrays, so why all the redundancy?

Upon further research, I realized while this does look like an associative array or a JS object…

  1. It's not an associative array, because JS does not actually support them unless we are synonymizing "associative array" and "object".
  2. It IS an array, which is why Array.isArray(array) returned true.
  3. EPIPHANY moment — arrays ✨ ARE ✨ objects. Hear me out

TL;DR

Here are the TL;DR versions of my answers to your questions. For more context and examples, keep scrolling…

  1. On the surface, it does seem inconsistent, but it's actually very consistent when you take a deeper look at the prototype chain and primitive types vs. reference types.
  2. Your array is an array even behind the scenes, but arrays in JS are a type of object and thus can be assigned custom properties in addition to their usual array elements.
  3. Nope! No specific engine needed for this. This is just pure JavaScript.
  4. If you require keys to be numeric and alphanumeric, use an object. HOWEVER, if you would like to use an array, it's perfectly okay to add custom properties to that array as well. Just keep in mind they are only properties of the object, not array elements.

If you require keys, and you also want to maintain the insertion order of your keys (since JS objects cannot reliably retain their property order), consider using a Map object-type instead.

The long version

Almost everything in JavaScript is some sort of object in one way or another, especially if you start exploring the __proto__ chain (MDN docs). In the same way, your array is an array, but it's also an object, just as all arrays are.

However, when you add a value to your array with a key like array["hi"] = "weird", you're not actually pushing a new value to the array, or even a key to the object for that matter in the usual JS objects kind of way, but rather, you are adding a property to the object that the array actually belongs to. So you will still be able to call that property like this:

array.hi // -> "weird"

…but this item is not actually an element in your array, so the below array, even though it's full of properties still only has a length of one.

const array = [];
array.push(1);
array['a'] = 'funky';
array.length; // -> 1

Even more interesting, this only happens when trying to pass an index with an alpha character in it. Even if you pass a numerical string, it will still push the value to the array in the usual/expected way. It's when alpha characters are present that arrays fall back to assigning properties instead of pushing array items.

Stranger yet — you can not only reference these properties without them "counting against" your array length, but you can actually store entire arrays and objects in these properties too as you would any other JS object.

const array = [];
array.push(1);
array['a'] = { b: 'test1', c: ['test2', 'test3']};
array.a.c[1]; // -> "test3"
array.length; // -> 1

Answers to your questions

1. Why have this behavior? This seems inconsistent, right?

Yes and no. This behavior is actually there to be consistent with the object-oriented nature of JavaScript as a whole. At the same time, as you mentioned, this is not consistent with the usual usage of JavaScript arrays and in most cases would likely baffle another developer working on a project where this was used in practice, so I wouldn't advise assigning properties to an array as a means of entry into that array, as it does not function that way.

However, properties are useful in all objects, and it is perfectly safe to add custom properties to any object as long as you are careful not to overwrite existing ones you might need. When you initialize an array [], it has a length of 0, right? Right. But does it already have inherent custom properties? Yep!

If you run the below code, you get ["length"] as a property name that already exists on an empty array.

Object.getOwnPropertyNames([]); // -> ["length"]

Similarly, if you add your own properties, they will also show up when calling this method, along with your array elements:

let array = ['a','b','c'];
array['test'] = true;
Object.getOwnPropertyNames(array); // -> ["0", "1", "2", "length", "test"]

More practically, you may have a time when you want to add a special function to an array in your project but not necessarily want that function to spread across all other arrays. Rather than implementing this as a new prototype function, you can add this new function to a specific array by adding the function as the value of a property as you did in your example. Your properties can even interact with each other.

let array = ['a','b','c'];
array.doubleLength = function() { return this.length * 2 };
array.hasAtLeast = function(x) { return this.length >= x };

array.length; // -> 3
array.doubleLength(); // -> 6
array.hasAtLeast(1); // -> true
array.hasAtLeast(3); // -> true
array.hasAtLeast(4); // -> false

2. What is the data structure actually storing behind the scene: as an array or something like object, hashtable, LinkedList?

I discussed this in more detail above as well. Essentially, what is happening here is that the new properties are being stored as object properties, but not in the usual object { key: value } key-value pair sort of way. Arrays and objects are both actually different types of what we'll refer to here as "raw objects". Everything in JavaScript is some instance of a raw object in one way or another. Arrays are a type of "raw" object. Key-value pair objects are a type of object and are also not these "raw" objects we are talking about, but another manifestation of them.

All objects shared a few key attributes. One of those attributes is that they all share the same ability to store properties, which we normally recognize as the keys in the objects we work with on a daily basis. All objects allow the addition of custom properties.

Some types of objects like strings require you to traverse to their prototypes in order to add properties. Because of this, this won't work:

let string = "Hello World";
string.addExclamation = function() { return this + "!" };
string.addExclamation() // -> THROWS ERROR!!!

…but this WILL work:

let string = "Hello World";
string.__proto__.addExclamation = function() { return this + "!" };
string.addExclamation(); // -> "Hello World!"

Other types of objects including both key-value objects and arrays, allow you to add custom properties directly to them without traversing to their prototypes, as I mentioned in my answer to your first question above.

This distinction between which types of objects allow the direct adding of properties and which don't, and which we'd consider "raw" objects and which we wouldn't boils down to a JavaScript ideology known as "primitive types" and "reference types". I highly suggest reading up on that "primitive vs. reference" distinction more.

It's also useful to note how to set up these prototype properties and the difference between prototype and __proto__. In many ways, prototype and __proto__ work the same, the key difference being that you can call the __proto__ property on any variable/object to get the prototype for its object type. On the other hand, prototype can be called only on the actual constructor for a variable/object. The below two prototype and __proto__ lines actually call the same object.

const string = "";
string.__proto__; // -> String { … }
String.prototype; // -> String { … }
string.__proto__ === String.prototype; // -> true

3. Whether this behavior depends on a specific Javascript Engine (V8, SpiderMonkey, etc..) or not?

Nope! This is about as raw as it comes when working with JavaScript. Just the beauty of object-oriented programming. I explain this in a lot more depth in the above two answers to your questions, as well as the content I added before my answers.

4. Should I use an array like this (keys are both numeric index and string) over a normal object?

Contrary to what a lot of answers are saying, yes do use arrays like this IF your situation calls for it. I don't mean to contradict myself here. Earlier, I did say you may want to avoid this, and that's true, so allow me to elaborate.

  • Should you add elements to arrays like this, where it adds them as properties and not as array elements? No. Adding to arrays like this is actually adding JS properties to them, not array elements.
  • Is there ever a case where I SHOULD add properties to arrays like this? Yes! It may not be often, and I explain this in more detail in my response to your first question, but if you are in a situation where you want to add custom properties, even functional properties to one specific array and not the Array.prototype which would affect all arrays, this is a perfectly fine and safe way to do so. I'll add my same example from my answer to question #1 below for reference:
let array = ['a','b','c'];
array.doubleLength = function() { return this.length * 2 };
array.hasAtLeast = function(x) { return this.length >= x };

array.length; // -> 3
array.doubleLength(); // -> 6
array.hasAtLeast(1); // -> true
array.hasAtLeast(3); // -> true
array.hasAtLeast(4); // -> false

For further reading, I would highly encourage reading my post in its entirety and chewing on it, and then checking out the MDN docs on the prototype chain. For even further study, I highly suggest signing up for Just JavaScript, a completely free 10-part email digest series and takes you on a deep dive into JavaScript Mental Models, which focuses largely on this prototype chain and other fascinating parts of what makes JavaScript so robust. It's written by Dan Abramov, one of the co-creators of Redux, and beautifully illustrated by Maggie Appleton. Definitely worth a peek!

Brandon McConnell
  • 5,776
  • 1
  • 20
  • 36
  • 1
    "*Is there ever a case where I SHOULD add properties to arrays like this?*" -> no. Either create `class MyArray extends Array { doubleLength() { return this.length * 2; }` or make a function `doubleLength = arr => arr.length * 2`. Adding random properties to an array is *very risk-prone!* At any point you might do `a2 = array.slice()` (because you realise you want a shallow copy) which might even just be modifying `someFunction(arr)` to `someFunction(arr.slice())` - so not even visible *inside* `someFunction`. Then you'd lose your custom properties and with them the custom behaviour. – VLAZ Apr 21 '21 at 08:34
  • "*properties are being stored as object properties, but not in the usual object { key: value } key-value pair sort of way*" this doesn't make much sense. It *is* the usual way. Well, from the perspective of any JS code. What happens in the engine might differ but the behaviour is identical - any array can be used the same as any object. It still just has key-value pairs. – VLAZ Apr 21 '21 at 08:41
  • "*Some types of objects like strings require you to traverse to their prototypes in order to add properties.*" string **primitives** are not objects. What happens when you use a property on them is that they are (briefly) converted to objects. Well, at least conceptually - what happens might be JIT magic. But regardless, the behaviour is that `str.someProperty` works as if doing `(new String(str)).someProperty`. However if you do `strObj = new String("hello")` you can still do `strObj.addExclamation = function() { return this + "!" }` and then `strObj.addExclamation()` works. – VLAZ Apr 21 '21 at 08:54
  • @VLAZ I am trying to run your code `class MyArray extends Array { doubleLength() { return this.length * 2; }` in my console but it's not firing for me. Should it work as is? – Brandon McConnell Apr 21 '21 at 13:02
  • @BrandonMcConnell - there is something in the markup of VLAZ's comment, just retype the code and it should work just fine – Oleg Valter is with Ukraine Apr 21 '21 at 13:38
  • @VLAZ - an off-note, since we are already in the new syntax territory, I would go with the getter instead of the function to mirror the `length` property (but I get this is just an example to illustrate the point). – Oleg Valter is with Ukraine Apr 21 '21 at 13:40
  • @BrandonMcConnell [JSBin demo](https://jsbin.com/dacidiv/1/edit?js,console) EDIT also, to convert a vanilla array to `MyArray`: `const converted = MyArray.from(vanillaArray);` then you can call `converted.doubleLength()` – VLAZ Apr 21 '21 at 13:59
  • 1
    @OlegValter I was mirroring the code in the answer. But you're right. Still, the example itself is simplistic in nature. Point being that if you want some custom behaviour for arrays you don't need to modify random vanilla objects. I'd really just go with functions as it's way easier to compose them and derive custom behaviour, even on the fly. They also fit nicely with existing array methods: `arr.map(doubleLength)` looks much better than `arr.map(x => MyArray.from(x).doubleLength())`. – VLAZ Apr 21 '21 at 14:08
  • 1
    @VLAZ - sure, my comment was more for the benefit of other readers (as I am sure you know - once you post something, somebody is bound to use it without thinking twice). Actually agree and prefer to use a functional approach to extending built-in objects. Cleaner, simpler, and allows for passing in anything that conforms to the function's contract, not just a specific class instance. – Oleg Valter is with Ukraine Apr 21 '21 at 14:12
  • 1
    Regarding your last example, one could say that a true OOP adept would create and populate the custom implementation beforehand and not during mapping (or even go as far as to add a `doubleMap` method), so the `x => x.doubleLength()` looks cleaner (or `x.doubleLength.bind(x)`), but, of course, they lose the beauty of the declarative approach. – Oleg Valter is with Ukraine Apr 21 '21 at 14:16
1

Other answers look good.

In JavaScript, an array is an object.

For example, you can destructure an array as an object due to each item in an array is an object with key-value pairs.

const array = ["Hello", "World"];
const {0: first, 1 :second} = array;
console.log({first, second});

And in case that the keys of each item in an array including integer, string, etc. You can object destructure by a specific key.

var array = ["hello"];
array["hi"] = "weird";
const {
        0: first,  // integer --> index number
        hi: second // string  --> string key
      } = array;
console.log({first, second});

Besides, you can only access an item by index when key is an integer.

var array = ["hello"];
array["hi"] = "weird";
console.log(array[0]);    // hello
console.log(array[1]);    // undefined
console.log(array["hi"]); // weird
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
  • "only access an item by index when key is an integer." - I would add an `a[0.5]` example to illustrate the point as it is the only part where "integer" is important. Because there is only one numeric (ok, now 2 with BigInt) data type in JS - Number which is a double-precision float, and the only time they are ints are during bitwise ops (where they are converted to 32-bit and back), which is why shifts are useful in truncating a number (e.g. `22.5 << 0 === 22`) – Oleg Valter is with Ukraine Apr 21 '21 at 17:20
1

Answer to question 1 and 2

Arrays are so-called "Integer Indexed Exotic Objects" in ECMAScript which are based on which in return belong to the object data type. Exotic objects are objects with some special behavior such as integer indices in our case. As far as I can tell, since we set a string index, our array is no longer an integer indexed exotic object.

Answer to question 3

Since this is ECMAScript, my understanding is, that all JavaScript engines have to implement it this way to stay compatible with the specification.

Answer to question 4

You should definitely not. Arrays should only have integer indices as per spec ("Integer Indexed Exotic Objects"). If you want to use string or mix both, use an object. When using an object, integer indices are coerced to string indices.

shaedrich
  • 5,457
  • 3
  • 26
  • 42
  • 1
    @NguyễnVănPhong I updated my answer to reflect statements to all of your questions. – shaedrich Apr 14 '21 at 11:53
  • 3
    A string exotic object is nothing related to arrays with string properties. It's just `new String("hello")` - the object has indexes that correspond to each character and a length. That's what a string exotic object is. Also, arrays are not an integer indexed exotic object. That refers to *other* objects with...integer indexes. An array is, unsurprisingly, [an array exotic object](https://262.ecma-international.org/11.0/#sec-array-exotic-objects). Adding non-integer indexes to it does not change that. – VLAZ Apr 21 '21 at 06:59
  • 1
    "*When using an object, integer indices are coerced to string indices.*" **this also happens with arrays**! Arrays *are* objects, so indexes for arrays are still just object keys. Object keys can only be strings or Symbols (latter is ES6+). Anything else is coerced to a string. *This includes numbers!* Not all string keys are equal, though - ones that contain a non-negative integer are occasionally treated specially, for example by ordering them. Other than that, `"1"` and `1` or even `{ toString() { return "1"} }` used as an index are *identical* as all convert to `"1"` in the end. – VLAZ Apr 21 '21 at 10:05
  • [Demo of keys always being coerced to strings](https://jsbin.com/mafixaf/1/edit?js,console) – VLAZ Apr 21 '21 at 10:08
  • What indicates that they are coerced to strings in your demo? All I can see is that they are treated to same. – shaedrich Apr 21 '21 at 10:10
  • @shaedrich all three values fetch the same index from the array. `"1"`, and `1` and `{ toString() {return "1";} }`. The only way they can do that is if the three are fetching the same key. The only way to fetch the same key is to convert them to the same type. That type is a string. – VLAZ Apr 21 '21 at 10:14
  • "The only way they can do that is if the three are fetching the same key." - right. But that's the only thing, the demo _shows_. It doesn't _tell_ whether the key is of type string or integer. – shaedrich Apr 21 '21 at 10:16
  • 2
    @shaedrich why do I get the impression that you don't believe me, yet you are also reluctant to support your answer here? [It's trivial to check how the object is converted](https://jsbin.com/geboyeh/1/edit?js,console). You can further see that a string is *not* converted to a number and that the keys *are* returned as strings. Given that it's *your claim* that all array keys are always integers, how about you try to show support for it? – VLAZ Apr 21 '21 at 10:36
  • @VLAZ I do _believe_ you. But I also want to _understand_ you. Thanks for that example. That emphasized better what's happening under the hood. – shaedrich Apr 21 '21 at 10:46
  • @shaedrich I am with VLAZ here. The ECMAScript spec defines what a property key is: "A property key value is either an ECMAScript String value or a Symbol value". It then defines that "An integer index is a String-valued property key that is a canonical numeric String (see 7.1.21) and whose numeric value is either +0 or a positive integral Number ≤ (253 - 1)" (emphasis on String-valued property key). Note that this is exactly what they said above when talking about coercion to string [1/3] – Oleg Valter is with Ukraine Apr 21 '21 at 11:00
  • 1
    [[Get]] internal method for the *integer indexed exotic objects* (which arrays are) accepts a *property key*. An abstract operation `ToPropertyKey` defines how input is coerced to a property key: via the ` ToPrimitive` with the hint `"string"` which means that if an object is passed as property key, the `toString` method is called (which is exactly what is shown in the second demo). It must be said that *technically*, the property keys that are *already* strings are converted to numbers as per the ``CanonicalNumericIndexString` abstract operation, but that only happens [2/3] – Oleg Valter is with Ukraine Apr 21 '21 at 11:28
  • if and only if the type of property key *is already a string* (clause 2 of 10.4.5.4 of the spec). All other property accesses are governed by the algorithm of the `OrdinaryGet` abstract operation. [3/3]. – Oleg Valter is with Ukraine Apr 21 '21 at 11:34
  • 1
    @OlegValter You say, "the integer indexed exotic objects (which arrays are)" while @VLAZ says the following: "Also, arrays are not an integer indexed exotic object." So which of the two statements is right? The second demo is totally fine. I said nothing to the contrary. "It must be said that technically, the property keys that are already strings are converted to numbers as per the ``CanonicalNumericIndexString` abstract operation" - yeah, that's what I was referring to at the beginning. – shaedrich Apr 21 '21 at 11:59
  • 1
    @shaedrich - Re:second demo - I just wanted to elaborate a little more, please don't consider this as a critique. Re:integer-indexed - true, I should amend myself, that's an issue on my part. IIEO refer to typed arrays, VLAZ's statement is correct. Read the previous comment as referring to the normal [[Get]] internal method with `OrdinaryGet` abstract operation. – Oleg Valter is with Ukraine Apr 21 '21 at 12:14
  • 1
    @shaedrich - btw, there is an interesting [open issue](https://github.com/tc39/ecma262/issues/1617#issuecomment-677718021) in the ECMA262 repo that basically equates IIEOs to typed arrays. – Oleg Valter is with Ukraine Apr 21 '21 at 12:16
  • 1
    @shaedrich - the only thing I would exclude from your answer is the reference to the string exotic object. Arrays definitely do not become SEOs (oh, god, what a great acronym) when string-indexed. SEOs is what allows the strings to be indexed and iterated als ob they were arrays. Here is an [example Q&A](https://stackoverflow.com/a/36384788/11407695) (surprisingly, not much info on string exotic objects is floating around) – Oleg Valter is with Ukraine Apr 21 '21 at 17:15
  • 1
    Thanks for the clarification. I gonna change that right away. – shaedrich Apr 21 '21 at 18:00
  • 1
    @shaedrich - thumbs up. I was really surprised how much it is said about array exotic objects and how little - on string exotic objects... – Oleg Valter is with Ukraine Apr 21 '21 at 18:09
  • Absolutely! Quite interesting. But the specs are tough reading. – shaedrich Apr 21 '21 at 18:49
  • 1
    Reminder than a normal array is ***not*** an Integer Indexed Exotic Object. It's instead [an Array Exotic Object](https://262.ecma-international.org/7.0/#sec-array-exotic-objects). The two are completely different aside from having indexes. An IIEO would be Typed Arrays which are a lot closer to an actual (non-JS) array than JS arrays are. Plain JS arrays (AEOs) act more like a list structure in other languages. The two are quite different. Also, adding a string property to an exotic object, doesn't change its type. – VLAZ Apr 22 '21 at 05:50
0

Note that this will only work in Chrome or in any other V8 engine browser. I tested your code in Safari and it didn't store the "hi" key-value pair at all. But in Safari it is rather an exception than the rule since you don't get an error either.

The reason why it works is that it is still an object. The functionalities of arrays are stacked on top of what objects in general have.

typeof array // "object"

Edit

  1. By the advice of @Kevin B, I realized that Safari indeed doesn't handle it differently than Chrome. The console.log implementation must be different in both browsers in this regard. I was still able to access the key value pair even if it wasn't shown when printing the array in the console.

  2. Also I noticed that the OP is looking for an answer from a reputable source. The only reputable source that I came across which exactly reflects what I wrote above is from w3schools.com

Arrays are Objects

Arrays are a special type of objects. The typeof operator in JavaScript returns "object" for arrays.

But, JavaScript arrays are best described as arrays.

Arrays use numbers to access its "elements". In this example, person[0] returns John:

[...]

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
MertDalbudak
  • 129
  • 8
  • 4
    w3schools is not a reputable source. – Kevin B Apr 14 '21 at 14:29
  • Instead of a historical account, you really ought to change the answer to what it would be if you answered it ***right now*** (the revision history will retain the historical information) – Peter Mortensen Apr 22 '21 at 11:12
0

Why does array allow string as an index in JavaScript

To add custom behaviour to an object

Question 1.1

Why have this behavior?

This is because array is itself an object with specific behaviour.

Array is a data structure which defines the behaviour of the object. So when we declare array we tell the compiler that this object must be treated as an array so the compiler considers numeric indexes for an array and everything else an arbitrary property. It can be negative integers or any string.

Read more about it here

For an array:

let array = []

If you write like this:

array["hi"] = "weird";

The compiler treats it as an arbitrary property of array.

length is one of the properties of an array which returns it's length.

Since we know that length is a property of array. Try running the below program you'll get the idea.

    let array = [];
    array[0] = "first";
    array[1] = "second";
    array[2] = "third";
    array[3] = "fourth";
    array["hi"] = "weird";

    // we overrided a predefined property of an array. You can try changing it's length and see what happens

    array["length"] = 3; 
    console.log(array, '\n Length of array: ', array.length);

Question 1.2

This seems inconsistent, right?

No, what if we want to add custom behaviour to the array. However, it can be done usnig 'prototype` but it applies for all the arrays but for specific array it might be needed. However, it is never needed.

Question 2

What is the data structure actually storing behind the scene: as an array or something like object, hashtable, linkedlist?

It is stored as an object since it is a data structure

Question 3

Whether this behavior depends on a specific Javascript Engine (V8, SpiderMonkey, etc..) or not? Behaviour of this won't change for javascript operations but handling of operations may differ.

If you run the above snippet in Console of Dev Tools in Chrome, the hi index is printed whereas it doesn't in the case of the Firefox and stackoverflow because they use different engines

You can read more about it here

Question 4

Should I use array like this(keys are both numeric index and string) over normal object?

No, it doesn't make sense to use array to store values in a (string)key when you cannot use the string keys in methods like forEach, map and array related methods. It'll make things complicated but still if you want to do it then use an object instead.

You can read more about it here

Abhishek Pankar
  • 723
  • 8
  • 26
  • 2
    "*the compiler only considers numeric indexes in an array.*" and "*in array, all properties are ignored by default*" are very misleading. Any property added to an array is still there. There is no special handling and specifically no ignoring that happens. However, *some* array operations will only work on positive integer indexes. This includes methods like `.forEach()` and `.map()` as well as `for..of`. However, that does not meant there is is special treatment for indexes that are non-positive integers. – VLAZ Apr 21 '21 at 08:57
  • Thanks for the feedback. Please edit the answer with fitting words. – Abhishek Pankar May 19 '23 at 17:54
-1

In JavaScript, there are two categories of data types: primitive types (number, string, boolean, bigInt, symbol, undefined, null) and objects (also known as Reference types).

An array is a special object that uses integers as its keys. JavaScript still treats it as an object. That means you can add/remove properties (That array will be called associative array or dictionary, like your example). If you want to truly treat it as a standard array, you should never use strings as keys.

Furthermore, you should use arrays as standard arrays because in JavaScript arrays have behavior and optimizations specific to their intended use. Also, when you add a new property with a string key to your array, the value of length will not change.

var array = ["hello"];    // Another way to declare a array is array = new Array("Hello"). This version clearly shows array is just a object.
array["hi"] = "weird";
console.log(array.length) // 1 not 2
array[1] = "A.Phong"
console.log(array.length) // 2
Vo Quoc Thang
  • 1,338
  • 1
  • 4
  • 5
  • Thanks for your answer. Could you give me some reputable resource? Besides, when should i use that structure over the object? – Nguyễn Văn Phong Apr 12 '21 at 13:30
  • 1
    I think you should treat array as a standard array and use plain object for dictionary as a rule of thumb. For resources, I strongly recommend you to read You don't know JS series https://www.amazon.com/gp/bookseries/B01N9EBP9V – Vo Quoc Thang Apr 12 '21 at 13:37