0

I encountered a weird JS behavior during a mock interview and I'm just curious why this occurs. Here is a minimal reproduce case:

class Association {}
class Document {}
class Post extends Document {}
class User extends Document {}

let [ u1, u2 ] = [new User(), new User()]
let [ p1, p2, p3 ] = [ new Post(), new Post(), new Post() ]

let posts = new Association(User, Post, "posts")

let database = {
  [u1]: { [posts]: [ p1, p2 ] },
  [u2]: { [posts]: [ p3 ] },
}

console.log("HERE", database[u1][posts]) // [ Post {} ]
Why is the result only 1 post? Shouldn't it be 2 as defined in the database?
Jack
  • 955
  • 1
  • 9
  • 30
  • 2
    What is your question? – pilchard Oct 19 '22 at 23:19
  • 1
    I wouldn't use `Document` as a class name since it is already a global property – evolutionxbox Oct 19 '22 at 23:19
  • 2
    Object properties must be strings. If you use something other than a string, it will be stringified. Both your `User` classes will serialise to the same value. If you want rich keys, use a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) – Phil Oct 19 '22 at 23:21
  • 1
    Would overriding the toString or hashCode method fix this? – Neil Shweky Oct 19 '22 at 23:24
  • 1
    See [Using an object as a property key in JavaScript](https://stackoverflow.com/q/12386450/283366) and [Javascript object literal - possible to add duplicate keys?](https://stackoverflow.com/q/38790146/283366) – Phil Oct 19 '22 at 23:24
  • @NeilShweky yes but each instance would have to return a different value to be used as individual keys within the same object – Phil Oct 19 '22 at 23:25

1 Answers1

2

Object property names can ONLY be strings or Symbols. We can ignore Symbols here. If you actually want to use objects as a key, then you can use a Map or a Set.

[u1]: { [posts]: [ p1, p2 ] },

This ☝️ line adds a property named [Object object] to the database object. It sets the value associated with that property to a new object that itself is initialised with a property named [Object object] whose value is set to be an array containing p1 and p2.

[u2]: { [posts]: [ p3 ] },

This ☝️ line overwrites the value of the property named [Object object] with a reference to a new object that has one property named [Object object] that has a value of an array containing p3.

When referring to an object property with the bracket syntax [<expression>], the expression is first evaluated, and the result is then coerced (because JS is weakly typed) to a string, if need be, to form the property name.

u1 and u2 are objects. Using an object as a property key will, by default, result in a call to the abstract internal operation OrdinaryToPrimitive with the hint 'string'. This will invoke Object.prototype.toString(), and this will, by default, result in the string '[Object object]'.

Note that you can override various aspects of this coercion behavior; for example, by implementing your own Symbol.toStringTag getter method:

class Association {}
class Document {}
class Post extends Document {}
class User extends Document {}

let [ u1, u2 ] = [new User(), new User()]
let [ p1, p2, p3 ] = [ new Post(), new Post(), new Post() ]
let posts = new Association(User, Post, "posts")

Object.defineProperty(u1, Symbol.toStringTag, {
  get() { return 'u1' }
})

let database = {
  [u1]: { [posts]: [ p1, p2 ] },
  [u2]: { [posts]: [ p3 ] },
}

console.log("HERE", JSON.stringify(database))

More details here.

Ben Aston
  • 53,718
  • 65
  • 205
  • 331