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.
Related
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.
This question already has answers here:
Why can I access object property with an array?
(2 answers)
Closed 2 years ago.
I ran into a scenario where JavaScript behaves in a way that is somewhat baffling to me.
Let's say we have an object with two keys foo & bar.
a = { foo: 1, bar: 2 }
Then, I have an array of strings, in this case one 'foo'
b = ['foo']
I would expect the following:
a[b] == undefined
a[b[0]] == 1
BUT, this is what happens:
a[b] == 1
a[b[0]] == 1
Why does JavaScript convert ['foo'] -> 'foo' when used as a key?
Does anyone out there know the reason?
How can this be prevented?
let a = { foo: 1, bar: 2 }
let b = ['foo']
console.log(a[b] == 1) // expected a[b] to be undefined
console.log(a[b[0]] == 1) // expected a[b] to be 1
All the object keys are string, so it eventually convert everything you place inside [] (Bracket notation) to string, if it's an expression it evaluates the expression and convert it's value to string and use as key
console.log(['foo'].toString())
Have a look at this example to understand, here [a] eventually converts a toString using a.toString() and then set it as key to b object
let a = { a : 1}
let b = {
[a] : a
}
// object converted to string
console.log(a.toString())
// object built using [] computed property access
console.log(b)
How can i stop this
In practical scenarios you should never do this, but just to illustrate, you can intercept or override the toString method of your object and return value as string with [] around:
let a = { foo: 1, bar: 2 }
let b = ['foo']
b.toString = function() {
let string = this.join(',')
return "[" + string + "]"
}
console.log(b.toString())
console.log(a[b])
When using an array as a key, javascript call the 'toString()' method of that array, and then try to find the stringified version of the array as the key. And if you call ['foo'].toString() you see this method returns "foo".
Why does JavaScript convert ['foo'] -> 'foo' when used as a key?
Does anyone out there know the reason?
Any time there is confusion as to why JavaScript acts in a way which may be unexpected, then looking at the language definition is the surefire way to exactly figure out what happened.
https://www.ecma-international.org/ecma-262/10.0/ is the most current language definition at the time of posting this.
First, you will want to find the area pertaining to Array access. It is in language lingo though.
12.3.2.1 Runtime Semantics: Evaluation
MemberExpression : MemberExpression [ Expression ]
...
3. Let propertyNameReference be the result of evaluating Expression.
4. Let propertyNameValue be ? GetValue(propertyNameReference).
6. Let propertyKey be ? ToPropertyKey(propertyNameValue).
So, what is happening here is you are accessing your array (the MemberExpression) using [] with an Expression.
In order to access with [] the Expression will be evaluated, and then GetValue will be called. Then ToPropertyKey will be called.
propertyNameReference = Evaluate Expression b = b
propertyNameValue = GetValue(propertyNameReference) = ['foo']
propertyKey = ToPropertyKey(propertyNameValue) = 'foo'
ToPropertyKey, in our situation, leads to ToPrimitive and then to ToOrdinaryPrimitive which states that we should call "toString" on the argument (['foo'] in our case).
This is where the implementation comes in to play. On the implementation side,
The Array object overrides the toString method of Object. For Array objects, the toString method joins the array and returns one string containing each array element separated by commas" MDN - Array toString
When there is only one value in the array, the result will simply be that value.
How can this be prevented?
This is the current way it is implemented. In order to change that, you must either change the default implementation, use detection to prevent the call, or use guidance to prevent the call.
Guidance
Document and enforce calling mechanisms in your code. This may not always be possible. It is at the very least reasonable to expect programmers to not call property access with arrays though.
Detection
This will depend on the current environment. With the most recent iteration of JavaScript, you can use type enforcement to ensure that property access is Number or String. Typescript makes this rather easy (here is a good example). It would essentially just require the access to be defined as:
function ArrayAccess(value: string | number) {
and this would prevent anyone from using the array as an accessor value.
Default Implementation
Changing the default implementation is a terrible idea. It will more than likely cause all sorts of breaking changes, and should not be done. However, just for completeness, here is what it would look like. Primarily I am showing this so you can hopefully recognize it if you see it somewhere and then kill it with fire (or check in some code to fix it if there were no spiders near it).
var arrayToString = [].toString;
Array.prototype.toString = function(){
if(this.length === 1) return;
return arrayToString.call(this);
};
Changing the instance implementation is not much of a better idea either. That is covered by #Code Maniac in a separate answer. "In practical scenarios you should never do this" #Code Maniac states, which I also agree with.
When using an array as a key, javascript call the 'toString()' method of that array, and then try to find the stringified version of the array as the key. And if you call ['foo'].toString() you see this method returns "foo".
Can somebody explain the behaviour of the following code?
let obj = {a:1, b:2}
let i = ['a']
console.log(obj[i])
>> 1
Why is it that even an array can be used to access a property inside an object? As a side note this only works with an array of length 1. I have tried researching this but there's no documentation as far as I know that explains why this should work.
Property names are always strings or symbols.
If you pass something which isn't a string or symbol, it gets converted to a string.
The default toString() method on an array is roughly:
String.prototype.toString = function () { return this.join(","); }
So ['a'] gets converted to 'a'.
As a side note this only works with an array of length 1.
It works fine with arrays that are longer. You just need a matching value:
const o = {
"a,b": "Hello"
}
const a = ["a", "b"];
console.log("" + a);
console.log(o[a]);
And since any object can be converted to a string, and you can customise the toString method, you can do really weird things:
const data = {
"42": "Hello"
}
class Weird {
constructor(x) {
this.x = x;
}
toString() {
return this.x + 40;
}
}
const w = new Weird(2);
console.log(data[w]);
(Note that doing really weird things is usually a stupid idea that makes it hard to debug your own code two weeks later).
let obj = {a:1, b:2}
First you declare an object with 2 properties, a and b with values 1 and 2, respectively.
let i = ['a']
Then a variable i is declared, with it's value set to a string array with a single element, 'a'.
console.log(obj[i])
In this statement, the value of i is resolved to a string because the array only contains one element, as you mentioned. Due to this, 'a' is a valid property name for obj because all object properties are either strings, and if you pass something such as an array, it's converted to a string.
If you reference the variable icontaining the array, and it has multiple elements, it cannot resolve to a single property name without you being more explicit such as obj[i[0]]. If you only have one value, it'll resolves to that value.
obj['a'] is a valid property
obj['a,b'] is not.
I am confused about placement of operators:
Given:
var a = [0, 1, 2];
So far as I can tell, each of the following is correct:
var len = a.length;
var lastElt = a.pop();
var str = String(a);
var typeStr = typeof a;
Is there an easy way to remember/think about whether the operator goes before or after the operand and whether it requires parentheses? Or is it simply rote memorization?
Thanks.
Let's go:
case #1
var len = a.length;
In this case you're calling the method lenght of the array store in the variable a.
case #2
var lastElt = a.pop();
As before, here you're calling the method pop of the array.
case #3
var str = String(a);
You aren't not calling any array's method. Here you're casting the array. i.e. you are stringifying the array.
case #4
var typeStr = typeof a;
Here you also aren't calling any array's method. You are calling the typeof method of the window object and passing the array a as an argument.
As you can see. In the first two cases, you are calling array's methods. In the last two cases, you are calling window object's methods and passing your array as an argument.
Your list there mostly consists of things that are not operators:
length is a property belonging to Array objects. It returns the number of items in the Array.
pop() is a method belonging to Array objects. It removes the last item from the Array and retuns it.
String() is a global object which is the constructor for a String object. It takes any object as a parameter and converts it to a String
typeof is an operator. It returns a string that indicates the type of the subsequent operand.
For more reference, here is some information about JavaScript operators.
I highly recommend searching Google for some beginning JavaScript tutorials so you can learn the basic concepts.
Thanks for all the prompt replies. In reply to the question 'What do you mean by "operator" here?' I come from a background of mathematics and (lately) c programming. By "operator" I wanted to speak abstractly about anything that mapped its argument to something useful, using it to include methods, properties, etc., without enumerating them. My attempt to abstract these is probably the source of my confusion. I now understand that what is required is rote memorization, something like declensions in Latin ;-).
Operators refer to +,=*,/ etc.
I don't think there is an easy way to remember. If it is a 'property', there will be no parentheses. If it is a function, there will be parentheses. I always remember that length is a property of an array, and that push and pop are actions you can do to the array. Type casting always starts with the 'type' capitalized followed by what you want casted in the parenthesis. typeof is just a weird one.
I have a simple example on python:
programs = {}
if not programs.has_key(( program, time )):
programs[( program, time )] = 0
programs[( program, time )] = programs[( program, time )] + 1
How to use array as key in Javascript ?
This will "work". (but I don't recommend it)
var a = {};
var b = [1,2,3];
a[b] = 'hello';
// a[b] evaluates to 'hello'
// a[[1,2,3]] evaluates to 'hello'
// a['1,2,3'] evaluates to 'hello'
It works because when you pass the array [1,2,3] as the hash (map/associative-array) key, is being converted to the string '1,2,3' before performing the hash lookup. It should suit your needs as long as you don't need two different arrays of the same value to map to different hash values.
var c = [1,2,3]
// a[c] evaluates to 'hello' even though we never executed a[c] = 'hello'
// but b == c evaluates to false
// b & c are two separate objects with the same values, so when they
// get converted to a string for hashing, they return the same value from the hash
As it was mentioned, you'll need more than the standard JavaScript hash if you want to use object references as your keys.
Update
Based on the comment from #speedplane:
I suspect that JS calls toString() on the array when you pass it into a hash key. So you can easily test what you're actually going to get as your key:
["x", "y", "z"].toString(); // 'x,y,z'
["x,y,z"].toString(); // 'x,y,z'
[1,2,3].toString(); // '1,2,3'
[1,2,'3'].toString(); // '1,2,3'
[[1],[2],[3]].toString(); // '1,2,3'
[["x",1], ["y",2], ["z",3]].toString(); // 'x,1,y,2,z,3'
So again, I recommend that you don't do this unless you really understand what is going on. And even then, I wouldn't do it.
JavaScript keys are strings.
You need a WeakMap, or a custom method to map arrays to other objects.
I've written a library called array-keyed-map for doing this robustly in modern JavaScript. Unlike the other answers so far posted, it does not rely on serialising values into strings, but instead uses ES2015 Map objects, which can accept arbitrary values as keys.
I'll quote my answer to a different question for an implementation overview, so the method is preserved posterity in case the library disappears for some reason, or you want to implement it yourself:
Maintain a tree of Map objects. Each tree stores:
Under an internally-declared Symbol key: The value at that point in the tree (if any). The Symbol guarantees uniqueness, so no
user-provided value can overwrite this key.
On all its other keys: all other so-far set next-trees from this tree.
For example, on akmap.set(['a', 'b'], true), the internal tree
structure would be like—
'a':
[value]: undefined
'b':
[value]: true
Doing akmap.set(['a'], 'okay') after that would just change the
value for the path at 'a':
'a':
[value]: 'okay'
'b':
[value]: true
To get the value for an array, iterate through the array while reading
the corresponding keys off the tree. Return undefined if the tree
at any point is non-existent. Finally, read the internally declared
[value] symbol off the tree you've gotten to.
To delete a value for an array, do the same but delete any values
under the [value]-symbol-key, and delete any child trees after the
recursive step if they ended up with a size of 0.
Why a tree? Because it's very efficient when multiple arrays have the
same prefixes, which is pretty typical in real-world use, for working
with e.g. file paths.
Will this do the trick for you?
jsfiddle
<script>
var ary = {person1:'valerie', person2:'alex'};
for (key in ary) {
document.write(key, '<br>')
}
document.write(ary['person2'], '<br>')
</script>