javascriptes6-map

Objects as keys in maps: Memory duplication?


I would like to map from objects that already exist elsewhere in the code, to, say, numbers.

var map = new Map();
var keyObj = {a: "b"};

map.set(keyObj, 3);

Does keyObj now exist twice in memory? Or is only a address to the original keyObj stored in the map?


Solution

  • TL;DR

    No.

    Longer Answer

    Explaining why that is requires talking a little bit about how Maps work. Maps are associative data structures, they associate keys with values. In order to do this they use a hashing function on the key to compress (or expand!) the key to a consistent width to associate that key with a particular region of memory. You can give the Map a key and a value and then later use the same key to update the same value. But in order for that later retrieval to work properly, the key has to hash to the same hash code. So what about the following:

    const a = {};
    const b = new Map();
    b.set(a, 3);
    a.foo = 'whatever';
    console.log(b.get(a));
    

    Note that {} and { foo: 'whatever' } do not hash to the same value when run through a hashing function. If you expect console.log(b.get(a)); to work properly after we've mutated a, we need some form of identity that is stable even when the key is mutated in place.

    Different languages solve this differently, for example in Python you simply aren't allowed to use mutable structures as hash keys and you'll get an exception if you try. Javascript has a notion (like most languages) has a notion of referential equality: the JS runtime associates stable identities with objects over time for you. We don't generally have access to those in our Javascript programs, but they make things like === work: comparison happens based on the stable unchanging value of the reference not the current value of a mutable object. Since that reference identity is stable it also works as the value that gets hashed for a Map or Set.

    This does have some counterintuitive properties though, like causing {} === {} to be false or the following:

    const a = {};
    const b = new Map();
    b.set(a, 3);
    console.log(b.get({})); // logs undefined
    

    But since we don't need the entire object, only the ref id, there's no excessive duplication of memory involved.

    Related answer I wrote when somebody asked a very similar question in the context of the Clojure programming language.

    Really, really going down the rabbit hole

    You mention address in your question, but note that the "address of the object" will not work for the purpose of being a stable identity for the exact same reason of mutability: what happens if you add a bunch of fields to an object that causes it to no longer 'fit'? The simple answer is that the runtime moves it somewhere else. In practice one trick for this is to use a double pointer: you hand out a pointer to a section of memory that contains the actual pointer to the start of the data, that way if you have to move it you don't invalidate the pointers to it that already exist. That outer pointer is, at the level we're having the conversation at, effectively the "address of the object" and could conceivably be used as a hashable value. But again, that's an implementation detail: the important concept to grok here is that of identity: what do we mean when we say two things are the same thing? How does that change over time? Those are the kinds of questions that shape the way these things work.