Why Does Array.Each Behavior Depend on Array.New Syntax

Why does array.each behavior depend on Array.new syntax?

This is a common misunderstanding. In your first example you are creating an array with 2 elements. Both of those are a pointer to the same array. So, when you iterate through your outer array you add 2 elements to the inner array, which is then reflected in your output twice

Compare these:

> array = Array.new(5, [])
=> [[], [], [], [], []]

# Note - 5 identical object IDs (memory locations)
> array.map { |o| o.object_id }
=> [70228709214620, 70228709214620, 70228709214620, 70228709214620, 70228709214620]

> array = Array.new(5) { [] }
=> [[], [], [], [], []]

# Note - 5 different object IDs (memory locations)
> array.map { |o| o.object_id }
=> [70228709185900, 70228709185880, 70228709185860, 70228709185840, 70228709185780]

Why Array.new(3, []) works differently than [[], [], []] in Ruby?

When using the .new method:

Since all the Array elements store the same hash, changes to one of them will affect them all.

If multiple copies are what you want, you should use the block version which uses the result of that block each time an element of the array needs to be initialized:

2.3.0 :001 > a = Array.new(3) { [] }
=> [[], [], []]
2.3.0 :002 > a.each_with_index{ |r, idx| r << 'a' }
=> [["a"], ["a"], ["a"]]

Read the examples here - https://ruby-doc.org/core-2.2.0/Array.html#method-c-new

Why does changing an Array in JavaScript affect copies of the array?

An array in JavaScript is also an object and variables only hold a reference to an object, not the object itself. Thus both variables have a reference to the same object.

Your comparison with the number example is not correct btw. You assign a new value to copyOfMyNumber. If you assign a new value to copyOfMyArray it will not change myArray either.

You can create a copy of an array using slice [docs]:

var copyOfMyArray = myArray.slice(0);

But note that this only returns a shallow copy, i.e. objects inside the array will not be cloned.

Trouble with arrays in Ruby

According to the Ruby Array docs:

http://ruby-doc.org/core-2.5.1/Array.html

Note that the second argument populates the array with references to the same object. Therefore, it is only recommended in cases when you need to instantiate arrays with natively immutable objects such as Symbols, numbers, true or false.

Which explains why

arr[0][0] = 'name'

Sets all keys to the same value. In your case the last-one wins so its time

What are you really trying to achieve? Setting a default value? If so, use the block syntax to pre-fill your array, like:

 arr = Array.new(6) { [2, '0'] }

Why does an array allow a string as an index in JavaScript?

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 /li>

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!

JavaScript new Array(n) and Array.prototype.map weirdness

It appears that the first example

x = new Array(3);

Creates an array with a length of 3 but without any elements, so the indices [0], [1] and [2] is not created.

And the second creates an array with the 3 undefined objects, in this case the indices/properties them self are created but the objects they refer to are undefined.

y = [undefined, undefined, undefined]
// The following is not equivalent to the above, it's the same as new Array(3)
y = [,,,];

As map runs on the list of indices/properties, not on the set length, so if no indices/properties is created, it will not run.

forEach loop quirky behavior with undefined values?


So apparently it is not assigning each value undefined, but only setting its length property to whatever is passed in the constructor.

Correct. (Provided you pass only a single argument and it's a number. If you pass a non-number, or pass more than one argument, they're used as elements for the array. So Array("3") results in ["3"]; Array(3, 4) results in [3, 4].)

This is not apparent when you log Array(n) to the console because it shows an array with n undefined values.

It depends on what console you use. The devtools in Chromium browsers show (3) [empty x 3] for exactly that reason, to differentiate between empty array slots and ones containing the value undefined.

So I guess my main question would be, are there other ways to execute forEach and map without filling the array or by using the quirky hack I mentioned earlier?

If you want forEach and map to visit elements of the array, they have to actually exist. Those methods (and several others) are defined such that they don't call your callback for empty slots in sparse arrays. If by "quirky hack" you mean [...Array(3)], that's also filling the array (and is fully-specified behavior: [...x] uses the iterator x provides, and the array iterator is defined that it yields undefined for empty slots rather than skipping them as forEach, map, and similar do). Doing that (spreading the sparse array) is one way to create an array filled with undefined (not empty) elements. Array.fill is another. Here's a third: Array.from({length: 3})





const a = Array.from({length: 3});
a.forEach(value => {
console.log(`value = ${value}`);
});

What’s the difference between Array() and [] while declaring a JavaScript array?

There is a difference, but there is no difference in that example.

Using the more verbose method: new Array() does have one extra option in the parameters: if you pass a number to the constructor, you will get an array of that length:

x = new Array(5);
alert(x.length); // 5

To illustrate the different ways to create an array:

var a = [],            // these are the same
b = new Array(), // a and b are arrays with length 0

c = ['foo', 'bar'], // these are the same
d = new Array('foo', 'bar'), // c and d are arrays with 2 strings

// these are different:
e = [3] // e.length == 1, e[0] == 3
f = new Array(3), // f.length == 3, f[0] == undefined

;

Another difference is that when using new Array() you're able to set the size of the array, which affects the stack size. This can be useful if you're getting stack overflows (Performance of Array.push vs Array.unshift) which is what happens when the size of the array exceeds the size of the stack, and it has to be re-created. So there can actually, depending on the use case, be a performance increase when using new Array() because you can prevent the overflow from happening.

As pointed out in this answer, new Array(5) will not actually add five undefined items to the array. It simply adds space for five items. Be aware that using Array this way makes it difficult to rely on array.length for calculations.



Related Topics



Leave a reply



Submit