Why negative indexing of an array in JS doesn't raise an error? It looks like it not intended to have elements with a negative index in an array:
array.length doesn't count elements with negative indices.
array.forEach() doesn't iterate over elements with negative indices.
UPD. The question is not "why it is thechnically possible" buth rather "why it is allowed by design".
[RESOLVED] Short answer: there is no particular reason, it just happened to become like this.
Arrays are the special type of object in JavaScript. It has an extra list of methods and properties (like .length and .forEach), and also it has a list of used indexes (integer positive number starting from zero higher).
But just like any other object, it can have additional properties:
var arr = ['A', 'B'];
arr.extra = 'C';
console.log(arr[0], arr[1], arr.extra); // A B C
Because of object properties can be accessed not only via dot but also via square brackets you can access any property using array-like syntax:
var obj = { extra: 'D' };
console.log(obj['extra']); // D
console.log(arr['extra']); // C
Using the same syntax you can assign properties:
obj['x'] = 'E';
obj[33] = 'F';
arr['y'] = 'G';
arr[-1] = 'H';
console.log(obj.x, obj[33], arr.y, arr[-1]); // E F G H
You can safely use numbers as a property name for the object, it will automatically be converted to a string.
The only difference is when you use positive integer values for the name of the property. Those are interpreted as array indexes.
var arr = [];
arr[0] = 'A';
arr[1] = 'B';
arr[-1] = 'C';
arr.forEach(value => console.log(value)) // A, B
console.log(arr.length); // 2
console.log( Object.keys(arr) ); // ["0", "1", "-1"]
Arrays in javascript also can work as hash objects (as almost all objects in js including functions). So doing: a[-1] = -1 simply defines a new key in the array (as object hash) with key value "-1" and value -1.
You should know that almost all objects in javascript can be used as hash objects as well. This is what you see. However these keys (which are not positive integers) for arrays do not count as array keys in the normal sense, only as hash keys.
JS allows negative indices in an array for the simple reason that they are objects under the hood. You can also put [].blah = 5 and it would be valid but a terrible idea.
Related
Say I have an array with string values such as:
var foo = ["Hello", "World"];
I can return the first character of each array element by doing:
foo[0][0]; // Will return "H"
foo[1][0]; // Will return "W"
However, when attempting to change the value of those characters using a similar method, it doesn't work. What I mean is that doing this does not work:
foo[0][0] = "J"; // Will not change "H" to "J".
It's not a huge issue since I know alternative ways to do so such as:
foo[0] = "J"+foo[0].substsring(1); // Hello --> Jello
But I'm curious as to why the previous method does not work? EDIT: Did some fiddling around and apparently it doesn't work with strings at all to begin with, not just strings in arrays. I guess I was under the false impression that strings act just like arrays. I can return certain characters by calling it's position in the string similar to how calling an array index works, but changing said index doesn't hold.
From Javascript String reference:
For character access using bracket notation, attempting to delete or
assign a value to these properties will not succeed. The properties
involved are neither writable nor configurable. (See
Object.defineProperty() for more information.)
Strings are immutable, that is, they cannot be altered.
This:
foo[0][0] = "J";
Doesn't work because you are attempting to modify the string stored at position 0 in the foo array.
This:
foo[0] = "J"+foo[0].substsring(1);
does work because you aren't trying to modify a string, you are trying to replace the element at position 0 in the foo array with an entirely new string.
All primitive data types in JavaScript are immutable.
Quote from MDN:
Unlike in languages like C, JavaScript strings are immutable. This means that once a string is created, it is not possible to modify it. However, it is still possible to create another string based on an operation on the original string.
Strings are immutable, so they cannot be changed unless you create a new string and store the new value inside of that string. You cannot change a character within a string, The string manipulation methods such as trim, substring etc.
From substring method - The result is a String value, not a String object.
var foo = ["Hello", "World"];
foo[0][0] = "J";
console.log(foo);
foo[0] = "J"+foo[0].substring(1);
console.log(foo);
Notice if you change
var foo = [1, "World"];
foo[0][0] = "J";
It works, since it's not a string
Check this out : https://tc39.github.io/ecma262/#sec-string.prototype.substring
If start is larger than end, they are swapped.
The following steps are taken: From the link above
Let O be ? RequireObjectCoercible(this value).
Let S be ? ToString(O).
Let len be the number of elements in S.
Let intStart be ? ToInteger(start).
If end is undefined, let intEnd be len; else let intEnd be ? ToInteger(end).
Let finalStart be min(max(intStart, 0), len).
Let finalEnd be min(max(intEnd, 0), len).
Let from be min(finalStart, finalEnd).
Let to be max(finalStart, finalEnd).
Return a String whose length is to - from, containing code units from S, namely the code units with indices from through to - 1, in ascending order.
I understand that map is not called on undefined indexes on arrays and I appreciate that an undefined index is different from an array index explicitly assigned the 'undefined' value (it is, isn't it?). Yet, how is one supposed to distinguish between holes in an array and undefined values?
The below code:
foo.js
var arr = [,,undefined,,,,3,,,4];
console.log("A hole the same as undefined? "+(arr[0]===undefined?"yes":"no"));
var _ignored = arr.map(function(x) {console.log("f called on ["+x+"]");});
... produces when running:
$ node foo.js
A hole the same as undefined? yes
f called on [undefined]
f called on [3]
f called on [4]
... similar results are to be had when replacing map with forEach.
The in operator tells you if an index has been actually assigned to:
a = []
a[5] = window.foo
document.write(a[4] + "<br>")
document.write(a[5] + "<br>")
document.write((4 in a) + "<br>")
document.write((5 in a) + "<br>")
Javascript arrays are actually objects with a special length property, and array indexes are just property names (in fact, they are even strings, not numbers, internally). So the above definition is equivalent to:
a = {
5: window.foo,
length: 6
}
Therefore, all object functionality related to keys (like in, hasOwnProperty, Object.keys) work for array indexes as well.
forEach and other iteration methods work by iterating from 0 to length-1 and checking if n-th index is actually present in the argument, they don't "know" if the argument is actually an array or just a generic object:
a = {1:'one', 5:'five', length:100};
[].forEach.call(a, function(x) {
document.write(x)
});
What you call a "hole" is not exactly a hole:
var a = [,,,,,,,,,,];
a.length; // 10
var n, k;
n=0; for (k in a){if (a.hasOwnProperty(k)){++n;}else{break;}};
n; // 10 : a has 10 own properties, all SET to undefined
Now delete all properties, and recount
while (n--) delete a[n];
for (k in a){if (a.hasOwnProperty(k)){++n;}else{break;}};
n; // 0
a.length; // 10 : a is identical to Array(10), empty array of length 10
To get an array with holes you can either delete properties by hand, or start with Array(N) and fill some indices with values, or use push/splice but not methods that return an array. For instance:
Array(2).concat([void(0)]).concat(Array(3)).concat([3]).concat(Array(2)).concat([4]);
reproduces exactly your original array (it has length 10 and 10 own properties), althought the first summand (Array(2)) has none.
EDIT : The above was tested in ExtendScript and is not true in recent browsers (see comments). Tested afterwards in Firefox and [,,,,,,] indeed has no own property...
This question actually is an outcome from another question, for which i have made some experiments, results are confused me more. I'm expecting answers which explains what actually happens internally.
What i have tried is,
I kept this as the base assumption because i got some clear explanation
here,
var a = [];
a['a1'] = [];
a['a2'] = [];
console.log(a); // []
console.log(a['a1']); // []
console.log(a['a2']); // []
TEST 1
This confused me a lot, since it prints b[0] as a and can be able to access length property, i thought var b can be treated as a character array, therefore i tried to assign another value , but ended up without success. From the base assumption, if this one can be treated as a char array(more generally as an array), the assignment should have been successful. It breaks the base assumption.
var b = 'abcd';
b['b1'] = [];
console.log(b); // abcd
console.log(b.length); // 4
console.log(b['b1']); // undefined
TEST 2
But if i create like this, the assignments are happens,
var bb = ['a', 'b', 'c', 'd'];
bb[4] = [];
console.log(bb); // ["a", "b", "c", "d", []]
console.log(bb.length); // 4
console.log(bb[0]); // a
console.log(bb[4]); // []
From this, i thought that, b[4] = []; may be successful, but
var b = 'abcd';
b[4] = [];
console.log(b); // abcd
console.log(b.length); // 4
console.log(b[4]); // undefined
My question is, why these assignments behaving differently while the variables sharing some common functionalities?
Here is the demo
Can anyone please give me a clear cut explanation about what actually happening internally?
Extra tests
Then if i tried with numerical assignment, it behaves quite differently form those two.
var c = 1234;
c[4] = [];
console.log(c); //1234
console.log(c.length); // undefined
console.log(c[0]); //undefined
console.log(c[4]); //undefined
When you access anything but an object with [], a temporary object instantiated with correct prototype (like String or Number) is provided for you. You can read and write properties of this object as you could with any other - try alert(3["constructor"]) for example. However, since this object is not referenced anywhere it goes to garbage right after you've done indexing it and next time you try to read same property you've just set, you're actually accessing a property on new temporary object, which, naturally, is empty.
Test 1: b is a string internally, not an array, so you can't assign something in a b position.
Test 2: Off course it works, since now bb is an array.
My question is, why these assignments behaving differently while the
variables sharing some common functionalities?
Because their types are different.
Test 3:
c is a Number, not an array.
Maybe you have some C background, where strings are char arrays terminated by a null char (\0). In JavaScript strings are built int types and they behave differently of arrays. The [] operator is just a convenience to access one especific char as Jonathan said. Here are some links, take a look:
JavaScript String
JavaScript Array
JavaScript Number
Reasoning
Your "Base Assumption" works because a is an array. "Test 2" is the only other test case that you have written that uses an array, which is why that is the only other test case that works.
You seem to be assuming that providing a square-bracket notation and having a length method indicates that an object is an array. This is not true. To test to see if an object is an array, you can use JavaScript's instanceof method, as follows:
var a = [];
var b = 'abcd';
var bb = ['a', 'b', 'c', 'd'];
var c = 1234;
a instanceof Array // => true
b instanceof Array // => false
bb instanceof Array // => true
c instanceof Array // => false
Notice that the cases where instanceof Array returns true are the ones that are acting as you expected, since you are trying to perform array operations.
Why "Test 1" Failed
For "Test 1", square-bracket notation for strings performs the string class's charAt function. Given b = 'abcd', performing b[0] is the same as performing b.charAt(0). It is read-only and simply returns the character at that index, which is "a".
See the Mozilla Developer Network documentation for Strings for more details.
Why "Extra Test" Failed
For your "Extra Test", integers do not provide a square-bracket notation or a length method, and thus all of those calls failed.
I guess I don't follow what your question is. Javascript treats strings, arrays, and integers differently. You should not expect them to behave in the same manner. Also there is no such thing as associative arrays in javascript to where your first example would work. The a['a1'] type of notation is actually only an alternative means for accessing object properties and have no meaning in the context of a string.
Are there any pitfalls to code like this?
var Foo = function() {
this.bar = function() { return 'bar'; };
};
var f = new Foo();
f[0] = 'hi';
f[1] = 'there';
Note that I'm creating a new function object with some misc properties, and then I'm treating the object like an array. Also how are the array values being stored in the object? Are 0 and 1 treated like property names?
Well, yes, 0, and 1 will be just two property names.
When you assign a property with the bracket notation, the expression between the brackets will be converted to String, and that string will be used as the property name.
In fact, even the indexes for real arrays are just that, properties:
var realArray = ['a'];
realArray.hasOwnProperty('0'); // true
The difference is that real array objects on every property assignment that correspond to a valid index[1], track internally the value of their length property.
That's one of the reasons why "subclassing" array objects is difficult, even with the new ECMAScript 5 extensions, or at the moment also with the proposed ECMAScript-Harmony Proxies 2 3, can't be completely done, in a stanard way.
That can be a problem, depending on how you plan to iterate the numeric properties.
If you enumerate the properties with the for-in statement, other members will appear, not just the numeric properties.
[1] A valid array index is any unsigned 32-bit integer in the range of 0 to (2^32)-1.
I think you might have problems if you try to loop through that with a for in loop; the loop will also get bar. There are ways around this, so you just have to be careful.
If you want to extend Array behavious please use : Array.prototype.yourfunc = function()....
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.