I was asked this on an interview, and I can't vouch for how Map is actually implemented under the hood (ended up here looking for myself). However, this is the approach I worked out with my interviewer (note, the requirement was only for DOM node keys, but I do think objects are the trickiest key to handle, and the rest could be handled trivially with additional code), and I think it is at least insightful:
class Map {
constructor() {
this.map = {}; // internal key/value object
this.trackerKey = Symbol();
}
set(key, value) {
let lookupKey = key[this.trackerKey];
if (!lookupKey) {
lookupKey = Symbol();
key[this.trackerKey] = lookupKey;
}
this.map[lookupKey] = value;
}
has(key) {
return key.hasOwnProperty(this.trackerKey);
}
get(key) {
const lookupKey = key[this.trackerKey];
return this.map[lookupKey];
}
delete(key) {
const lookupKey = key[this.trackerKey];
delete key[this.trackerKey];
delete this.map[lookupKey];
}
}
Basically, the idea is to use Symbols under the hood to keep track of the objects. trackerKey
is a (Symbol) property we add to all keys that are coming in. Since it's a symbol defined inside the instance, nothing else will be able to reference it.
So when we go to set
, we check if the trackerKey property exists on the object. If not, we set it to a new symbol. We then set the value of that key in our internal map to the value coming in.
has
and get
are fairly straightforward lookups now. We check if our tracker key exists on the key to see if it is included in our Map. For get
we can simply grab our internal lookup key for the object from its trackerKey property.
For delete
, we just need to delete the trackerKey property from the key object, and then delete the property in our internal map object.
Fun and insightful exercise! Hope this helps :)