Empty slots in JavaScript objects? - javascript

Lately I started seeing this in firefox' console
Object [ <6 empty slots>, false, <3 empty slots>, 1 more… ]
When I have an object like
{
6: false,
10: true
}
I simply want an object with numeric keys that I can access, but I am worried by this because if it keeps track of empty slots then this must mean that some memory is wasted?
Are my concerns valid and if yes what would be a correct way to define such an object?

The problem might be caused at how Firefox' console.log has interpreted the input object. Somehow, it got evaluated as an array instead of a simple object. Chrome does it right. If you look deeper into how an array is managed in Javascript, you can find the following:
Arrays cannot use strings as element indexes (as in an associative array), but must use integers. 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. src
A better comprehending for this is to tinker with Array's length property. Especially when you have constructed your array by using []. To add elements to the array, we have to use .push(...). This function uses the length property (check 15.4.4.7 Array.prototype.push). So in short (interactive example is at the bottom)
const arr = []; // length = 0
arr.push('1stEl', '2ndEl', '3thEl'); // length = 3
// this isn't allowed, but you can do this
arr[7] = '7thEl'; // length = 8
You see that the length is now 8 and not 4. The indices 3..6 are reserved, but undefined. Here below is a console output.
[
"1stEl",
"2ndEl",
"3thEl",
undefined,
undefined,
undefined,
undefined,
"7thEl"
]
If you use a .push method again, it will place the new element after the '7thEl' element (so on index 8).
To check the keys that is used by this object, we can use Object.keys() on the array. You will get
[
"0",
"1",
"2",
"7"
]
You see that numeric values are used as keys. Like your object, which is
{
6: false,
10: true
}
Using Object.keys on this object gives ["6", "10"]. It has a similar output as the above. So the console.log from firefox has interpret your object as an array, thus displaying it as an array. In order to display the array correctly, it starts (logically seen, need to check the source code yet) at key 0 and ends at key array.length - 1. But the indexes 0,1..5 and 7..9 aren't "defined". Thus it leads to this output
Object [ <6 empty slots>, false, <3 empty slots>, 1 more… ]
I'm not sure if I have to qualify this as a bug or glitch at Firefox's console API... Or that the console input (when initializing a variable) has read the object incorrectly.
--- live example --
const a = new Array(3);
console.log('using "new Array(...)" reserves memory space: ' + a.length);
console.log('---');
// using brackets
const b = [];
console.log('but what with [] ? At initial, we have ' + b.length);
b.push('1stEl', '2ndEl', '3thEl');
console.log('After push(\'1stEl\', \'2ndEl\', \'3thEl\'), we have ' + b.length);
// add to random index
b[7] = '7thEl';
console.log('After b[7] = \'7thEl\', we have ' + b.length);
console.log('displaying gives ', b);
console.log('using Object.keys: ', Object.keys(b));
// adding again
b.push('newEl');
console.log('After b.push(\'newEl\'), we have ' + b.length);
// object
const obj = {
6: false,
10: true
};
console.log('obj defined as {6: false, 10: true }');
console.log('using Object.keys: ', Object.keys(obj));
console.log('obj: ', obj);

Javascript uses sparse arrays. "Since an array's length can change at any time, and data can be stored at non-contiguous locations in the array, JavaScript arrays are not guaranteed to be dense; this depends on how the programmer chooses to use them." (source)
If the objects are of type Array, then the memory used is an implementation detail of the engine. In your case, the objects are objects, so it only takes the memory for the object itself, and to store the property names and references to property values.

Related

Javascript array of json prints empty but it has an object inside

I'm trying to understand why Javascript prints an empty array when it should have at least one value inside it. Please, check this code:
detail = [];
detail.cat1=[];
detail.cat2=[];
detail.cat3=[];
detail.cat4=[];
var newEntry = {"cod":"01","dt":"2021-10-02 09:07:21.205-07:00"};
detail.cat2.push(newEntry);
console.log(detail);
console.log(detail.length);
console.log(detail.cat2);
The results are:
> Array []
> 0
> Array [Object { cod: "01", dt: "2021-10-02 09:07:21.205-07:00" }]
How could it print [] since I have one object inside? And how can the length be zero? I was using any Javascript online editor, and the result is the same.
Since an array is really an object, we can add methods/properties directly to an individual array.
Then if you want to see the keys in an object for instance, Object.keys won't see it, but Reflect.ownKeys() will.
The Reflect version of defineProperty will return success or fail, while Object's returns the object passed.
Remember Reflect.preventExtensions() will (like it says) stop you or others from extending them.
As far as question about length is there, ".length" gives length of values not properties so it still gives 0
Array is an indexed collection of values, means you can put element at particular index and get element from particular index.
In the below example, you can see that if there are already elements in an array then you can get its length using length property.
Remember: The length of an array is (highest integer index at which the element is present in array + 1)
const arr = [1, 2, 3, 4];
console.log(arr); // prints whole array
console.log(arr.length); // prints length of an array
console.log(arr[2]); // Element at index 2
If you set element at particular index let say arr[8] = "something" then its length will be 9
const arr = [];
arr[8] = "something";
console.log(arr.length) // 9
It is true that array's are objects, so you can set/get values to/from array. But if you set property that is not a numbered index then it will not increase its length but It will set the property to that array object as:
const arr = [];
arr["introduction"] = "I'm an array";
arr.property = "This is a property"
console.log(arr.length);
console.log(arr.introduction);
console.log(arr.property);
When displaying an array, the default Array.prototype.toString method is usually called. This actually calls Array.prototype.join with no arguments, so produces a comma separated string of the elements of the array.
The join method only returns properties that have a positive integer name (an array index, indicating an array element), other properties are ignored by join.
Similarly, only elements of the array affect the length property, ordinarly properties (i.e. those that don't have an array index for their name) don't affect length.

forEach loop quirky behavior with undefined values?

Was writing a script in JS to make some dummy data for testing my API and ran into an interesting quirk with the forEach loop in JS.
const dictionary = {};
const undefinedArray = Array(3); // [undefined, undefined, undefined]
undefinedArray.forEach((_, index) => {
console.log('Logging at index: ', index)
const someObject = { id: index };
if (!dictionary[someObject.id]) {
dictionary[someObject.id] = someObject
}
});
console.log(dictionary);
After checking the output of this snippet, you'll see that nothing inside the forEach loop is logged and the dictionary is still an empty object. I was talking with my coworker about this behaviour and he said he ran into this particular issue before and offered this as a solution.
const dictionary = {};
const undefinedArray = [...Array(3)]; // [undefined, undefined, undefined]
undefinedArray.forEach((_, index) => {
console.log('Logging at index: ', index)
const someObject = { id: index };
if (!dictionary[someObject.id]) {
dictionary[someObject.id] = someObject
}
});
console.log(dictionary);
By wrapping the Array constructor in square brackets and utilizing the spread operator, now the array is looped through and the object is built correctly. This fascinated me, so I went to the documentation for the Array object and found this:
arrayLength
If the only argument passed to the Array constructor is an integer between 0 and 2^32 - 1 (inclusive), this returns a new JavaScript array with its length property set to that number (Note: this implies an array of arrayLength empty slots, not slots with actual undefined values). If the argument is any other number, a RangeError exception is thrown.
So apparently it is not assigning each value undefined, but only setting its length property to whatever is passed in the constructor. This is not apparent when you log Array(n) to the console because it shows an array with n undefined values. I assume the toString method for the Array object is based on its length property and uses a normal for or for of loop to construct the string.
It does begin to make a little bit more sense, however, when you explicitly set an index of the newly defined array. In the snippet below, the same array is initialized, but the zero index is explicitly assigned undefined as a value. Since this is an "actual undefined value" in Mozilla's words, the forEach loop exectues at index zero.
const dictionary = {};
const undefinedArray = Array(3); // [undefined, undefined, undefined]
undefinedArray[0] = undefined
undefinedArray.forEach((_, index) => {
console.log('Logging at index: ', index)
const someObject = { id: index };
if (!dictionary[someObject.id]) {
dictionary[someObject.id] = someObject
}
});
console.log(dictionary);
Array.map() behaves the same way. 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?
To recap: these are the two work arounds I've found for this particular use case:
[...Array(n)] OR Array(n).fill(). Both of these mutations to the array will allow a forEach loop to iterate over all values in the array.
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}`);
});
Which you use is up to you. Array.from is very simple and direct. Similarly Array(3).fill(). I probably wouldn't use the spread version (just because I think it's fairly unclear to people who don't have a deep knowledge of how the array iterator works), but it's a matter of style.

What values do the <empty> elements created by new Array() have?

I'm quite confused by these little guys. After I encountered some funny behavior between them and Array.prototype.filter I fooled around in re.pl trying to understand their true value. But it seems like they switch from <empty> to undefined depending on who's looking (at least in re.pl and node, they're logged as undefined in this environment).
let emptyArr = new Array(5);
//set up two control elements
emptyArr[0] = 0;
emptyArr[4] = undefined;
console.log('\nemptyArr:', emptyArr)
console.log('\npeeking at an empty element:', emptyArr[1])
console.log('\nfilter for undefined elements:', emptyArr.filter(e => e === undefined))
console.log('\nfilter for any element:',
emptyArr.filter(e => {
console.log("ele:", e)
return true
})
) // only two elements are registered here
console.log('\nmappedEmpty:', emptyArr.map(e => e)) //everything is preserved
console.log('\ngenerated array', Array.from(emptyArr))
console.log('\nalways true filter on generated array:', Array.from(emptyArr).filter(e => true)) // empties are now 'true' undefined
What's the story here? Quirky array prototype methods or a secret ultra-false-y value?
What's the story here? Quirky array prototype methods or a secret ultra-false-y value?
Arrays are objects. Elements of the array are simply properties of the underlying object. Accessing a property that doesn't exist returns undefined. Therefore when you access emptyArr[1] you get undefined. Looking at the console of Chrome might help:
As you can see, 0 and 4 exist because you created those entries by assigning to them. 1, 2 and 3 don't exist.
These positions with no value are often referred to as "holes". Your array has holes at positions 1, 2 and 3. An array with holes is also called "sparse array".
Most array methods (.filter, .map, etc) skip over holes. We can easily prove this for some methods:
// Array#map
console.log([,,,42].map(() => 21)); // [,,,21], not [21,21,21,21]
// Array#every
console.log([,,,42].every(x => x === 42)); // true, not false
Of course we could also just look at the language specification, where it says for Array#every for example:
callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.
Array.from on the other hand explicitly looks at the .length property of the value passed to it and it will copy any property/element between 0 and .length. In other words, it does not skip holes.
Look at the difference between the arrays in the Chrome console:
Worth noting maybe that arr.length doesn't care about holes. It will always be the highest set index in the array + 1.
Well actually they do not exist. Imagine arrays being objects, using new Array(5) you will get:
{
length:5
}
So like objects they return undefined for nonset values:
array[3] // undefined
But that doesnt mean theres a property set to undefined. That changes when you use Array.from . You can imagine it doing sth like
for(var i = 0; i < oldarray.lemgth; i++){
newarray[i] = oldarray[i];
}
So after the Array.from call itll look like this:
{
length:5,
0:undefined,
1:undefined,
2:undefined,
3:undefined,
4:undefined
}
They are undefined. It just depends on the engine how it displays them, but trying to access them returns undefined, because that's what they are - elements that have not been defined.
For example the Chrome console will print > (5) [empty × 5] while node prints [ , , , , ] for the same new Array(5). It's just a more visual representation than, say, showing [undefined, undefined, undefined, undefined, undefined].

Why do arrays behave this way when I add named properties?

I did following experiment in browsers console
I created a new Array.
Added "foo" to the Array as named index "name".
Added "bar" to the Array using push method.
4 & 5 are tests on the Array
1. var myArray = []; // undefined
2. myArray["name"] = "foo"; // "foo"
3. myArray.push("bar"); // 1
4. myArray.join(", "); // "bar"
5. myArray["name"]; // "foo"
My questions (what I didn't understand)
.push() returns 1 which is the length of the Array, but it must be 2 as the Array has two values "foo" & "bar"
Test 4 shows that the Array has only one value "bar" but test 5 oppose it showing that it also has a value "foo".
Why doesn't Array methods (push, join etc) works on key/value pairs ?
Then how does associative Array works and how we can handle it(methods, properties etc).
.push() returns 1 which is the length of the Array, but it must be 2 as the Array has
two values "foo" & "bar"
No, because arrays in JavaScript are not associative data structures (even though you can attach arbitrary properties to them). The only items that count as "array contents" are those whose property names satisfy certain conditions.
You should also be aware that length may also report a number greater than what you expect if the array has "holes". For example:
var a = [1, 2, 3];
console.log(a.join(" "), a.length); // "1 2 3", 3
delete a[1];
console.log(a.join(" "), a.length); // "1 3", still 3!
Test 4 shows that the Array has only one value "bar" but test 5 oppose it showing that it also has a value "bar".
That's irrelevant. The array also has many other properties, but join will only touch those mentioned earlier.
Why doesn't Array methods (push, join etc) works on key/value pairs ?
Because that's what the spec says.
Then how does associative Array works and how we can handle it(methods, properties etc).
Use a plain object instead of an array if you want string keys.
JavaScript doesn't really have associative arrays. Being able to do myArray["foo"] = "bar" is a side effect of arrays simply being objects, and objects having properties.
Only numeric properties count as elements of the array. If you do myArray["foo"] = "bar" you are not adding an element to the array, you're simply adding a property to the object. That's different to doing myArray[0] = "bar" which is adding an element, and will accordingly update the length property of the array object.
In JavaScript you cannot address array elements with a string, only integers. So,
1. var myArray = []; // undefined
2. myArray["name"] = "foo"; // "foo"
3. myArray.push("bar");
myArray will only contain "bar" since it ignores myArray["name"] = "foo"; which is why you get length 1.
Just like functions, Arrays are objects. But there are many Array prototype functions which purposefully do not access the properties of object. There is no 'associative array' in JavaScript, other than object and arrays depending on the function or code can behave or be leveraged in the same way you would use an associative array. You will encounter the same confusion in for loops when iterating over object key/pairs vs array values. In general though they are quite effective, there is a good book called Java Script the good parts. Which also then highlights the features or 'bad parts' to avoid or are dangerous. Googling about the sharp edges of objects and arrays in Javascript yields some good reads. They are not complex, but you do have to understand the more subtle ways Java is not like JavaScript.
Arrays in Javascript are not meant to be associative - when you add a property via a key you're actually adding it to the array object, not to the array itself.
If you want a key:value pair, use an Object

Javascript array length incorrect on array of objects

Could someone explain this (strange) behavior? Why is the length in the first example 3 and not 2, and most importantly, why is the length in the second example 0? As long as the keys are numerical, length works. When they are not, length is 0. How can I get the correct length from the second example? Thank you.
a = [];
a["1"] = {"string1":"string","string2":"string"};
a["2"] = {"string1":"string","string2":"string"};
alert(a.length); // returns 3
b = [];
b["key1"] = {"string1":"string","string2":"string"};
b["key2"] = {"string1":"string","string2":"string"};
alert(b.length); // returns 0
One thing to note is that there is a difference between regular arrays and associative arrays. In regular arrays (real arrays), the index has to be an integer. On the other hand, associative arrays can use strings as an index. You can think of associative arrays as a map if you like. Now, also note, true arrays always start from zero. Thus in your example, you created an array in the following manner:
a = [];
a["1"] = {"string1":"string","string2":"string"};
a["2"] = {"string1":"string","string2":"string"}
Javascript was able to convert your string indexes into numbers, hence, your code above becomes:
a = [];
a[1] = {"blah"};
a[2] = {"blah"};
But remember what i said earlier: True arrays start from zero. Therefore, the javascript interpreter automatically assigned a[0] to the undefined. Try it out in either firebug or the chrome/safari console, and you will see something like this when you try to print "a". You should get something like "[undefined, Object, Object]. Hence the size 3 not 2 as you expected.
In your second example, i am pretty sure you are trying to simulate the use of an associated array, which essentially is adding properties to an object. Remember associated arrays enable you to use strings as a key. So in other terms, you are adding a property to the object. So in your example:
b["key1"] = {"string1":"string","string2":"string"};
this really means:
b.key1 = {"string1":"string","string2":"string"};
Initializing b =[] simply creates an array, but your assignment doesn't populate the array. It simply gives "b" extra properties.
length returns 1 + the largest integer key in the object.
In a the largest key is 2 so 1+2 is 3.
In b there are no integer keys (the keys there are key1 and key2 which cannot be converted into ints) so Javascript assumes that the largest key is -1, and 1 + -1 yields 0.
This program will help you see that:
a = [];
a["1"] = {};
a["4"] = {};
alert(a.length); // Prints 5
From the ECMAScript standard, ECMA-262, 5th ed.
15.4.5.2 length
The length property of this Array object is a data property whose value is always numerically greater than the name of every deletable property whose name is an array index.
Note the length property of an array only takes into account array indices, which are integers; setting other properties doesn't affect length.
For an array, a["3"] is equivalent to a[3] (this behavior is specified by § 15.4.5.1); 3 is an array index rather than a property. Thus setting a["3"] affects the array's length. b["key1"] is equivalent to b.key1. Setting properties don't affect the length of a collection.

Categories