23

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
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
tompos
  • 249
  • 1
  • 7
  • 9
    This can be prevented by not using an array as a key. – QuentinUK Sep 15 '19 at 07:30
  • 1
    An easy way to prevent that would be to use `Map` instead of object. – georg Sep 15 '19 at 08:02
  • 8
    Simply because JavaScript converts *every* object to a string when used as an object key? Keys always must be strings (or symbols). – Bergi Sep 15 '19 at 15:08
  • 2
    This doesn't only happen to one-string-arrays. The only thing that makes them special is that their string conversion leads to the same string that is contained in them. But you can also try `a[b] = 1` with `b = ['foo', 'bar']` and will get `a['foo,bar'] == 1`. – Bergi Sep 15 '19 at 15:13
  • Not only object anything which is non string will be converted to string. i.e `let a = {1:'a'}; console.log(typeof Object.keys(a)[0] === 'string' )` – Code Maniac Sep 15 '19 at 17:39
  • 1
    fun fact, arrays are just exotic objects, so the following both yield 1: `[1][0]`, `[1]["0"]`. all keys are converted to strings when using objects. this is one reason where you might want to use a `Map` to store the true value of something as a key – Dan Sep 15 '19 at 18:52

4 Answers4

14

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])
skiwi
  • 66,971
  • 31
  • 131
  • 216
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
13

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".

Sinandro
  • 2,426
  • 3
  • 21
  • 36
3

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.

  1. propertyNameReference = Evaluate Expression b = b
  2. propertyNameValue = GetValue(propertyNameReference) = ['foo']
  3. 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.

Travis J
  • 81,153
  • 41
  • 202
  • 273
0

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".

Sandhya
  • 98
  • 10