17

I've read what a Map is, and understand the differences between in Object vs Map. What I don't understand is why I would ever use objects or functions as the keys which a Map allows.

Question: Why & When would I ever set an object or a function as a key?

nem035
  • 34,790
  • 6
  • 87
  • 99
  • 4
    Possible duplicate of [Maps vs Objects in ES6, When to use?](https://stackoverflow.com/questions/32600157/maps-vs-objects-in-es6-when-to-use) – juvian Jul 11 '17 at 17:54
  • You only would do this when you are working with the identity of `Object`s of a specific type. But I cannot think of a use case right now. Anyway, `Map` et al. would be much more useful if Javascript had [value objects](https://de.slideshare.net/BrendanEich/value-objects). –  Jul 11 '17 at 18:31
  • 3
    This question is much more specific than "_Maps vs Objects in ES6, When to use?_" and no duplicate! –  Jul 11 '17 at 18:34

3 Answers3

9

Basically if you want to track any information related to an object that should for some reason not be present on the object itself, you could use a map.

A simple example could be to track how many times an operation was done relating to an object.

Here's a demonstration where we keep track of how much food each animal (instance) has eaten without affecting the animal itself:

function Animal(type) {
  this.type = type;
}

// now let's say somewhere else in our program
// we want to have a farm where animals can eat.
// We want the farm to keep track of how much each animal ate but
// the animal itself doesn't need to know how much food it has eaten.
const AnimalFarm = (() => {
  const mapOfAnimalToAmountOfFood = new Map();
  return {
    feedAnimal(animal, amountOfFood) {
      // if the animal is being fed the first time
      // initialize the amount to 0
      if (!mapOfAnimalToAmountOfFood.has(animal)) {
        mapOfAnimalToAmountOfFood.set(animal, 0)
      }

      // add amountOfFood to the amount of food already eaten
      mapOfAnimalToAmountOfFood.set(
        animal,
        mapOfAnimalToAmountOfFood.get(animal) + amountOfFood
      )
    },
    getAmountEaten: function(animal) {
      return mapOfAnimalToAmountOfFood.get(animal)
    }
  }
})()

const dog1 = new Animal('dog')
const dog2 = new Animal('dog')

AnimalFarm.feedAnimal(dog1, 300)
AnimalFarm.feedAnimal(dog1, 500)
AnimalFarm.feedAnimal(dog2, 1234)

console.log(
  `dog1 ate ${AnimalFarm.getAmountEaten(dog1)} total`
)

console.log(
  `dog2 ate ${AnimalFarm.getAmountEaten(dog2)} total`
)

In general, a main reason for creating a map of objects to some data is that you can maintain local information about an object which, although directly related to this object, is fully contained in your own module and doesn't pollute any other parts of the system (Separation of Concerns).

Another example could be a graph which has a map of objects representing the nodes to a list of other nodes they have a connection to (useful for example in Dijkstra's algorithm):

Map<Place, ListOfPlacesICanGoTo>

This allows you to have a more pure Place object by separating this relationship rather than putting a direct Place.listOfPlaces link within the object itself. This is particularly useful if a Place is used in other contexts where listOfPlaces is not needed or even doesn't make sense.

Another common usage of objects as keys in a map-like structure is when using a WeakMap, because it's also more memory efficient by allowing each key object to be garbage collected as soon as nothing else references it. An example could be the underlying implementation for process.on('unhandledRejection') in node which uses a WeakMap to keep track of promises that were rejected but no error handlers dealt with the rejection within the current tick.


As far as using a function as a key, I would personally think this is less useful but certainly not useless.

One useful example could be to check if a certain function was already passed in before and not invoke it again but return a cached result. This could prevent repetitive execution of potentially expensive operations.

const map = new Map();
function invokeOrGetFromCache(fn) {
  if (map.has(fn)) {
    return map.get(fn);
  }
  const result = fn();
  map.set(fn, result);
  return result;
}

function exampleFn() {
  console.log('start');
  for (i = 0; i < 100000; i++);
  console.log('done');
  return true;
}

console.log(
  invokeOrGetFromCache(exampleFn) // runs exampleFn
);

console.log(
  invokeOrGetFromCache(exampleFn) // retrieves from cache
);

As with objects, using a WeakMap could be preferable in these situations as well, for efficiency reasons.

nem035
  • 34,790
  • 6
  • 87
  • 99
  • I guess my answer is just a more specific example of your last point. – Patrick Roberts Jul 11 '17 at 17:50
  • Your later example makes way more sense... but still doesn't explain why I would use the key as an `object` or `function` I see your checking if `map.has(fn)` but you could do the exact same checking for the `prop` in an `object`.... and if it exists don't set it. –  Jul 11 '17 at 17:51
  • 1
    @JordanDavis putting a property in an object, as was already explained in this answer quite thoroughly, makes it no longer "pure". It clutters it with useless information that could potentially cause memory leaks if not handled carefully, and also causes side-effects in serialization using `JSON.stringify()`, etc. – Patrick Roberts Jul 11 '17 at 17:53
  • I'm not understanding your definition of "pure" and how a property "clutters" in object, I guess.... just trying to understand what you mean by this. –  Jul 11 '17 at 18:01
  • 1
    @JordanDavis when you have a fairly large system, let's say hundreds of classes of objects with many relationships, using a map of objects to some data allows you to maintain information specific to an object without changing the actual object, which is much safer because changing it could potentially affect many other parts of the system. – nem035 Jul 11 '17 at 18:04
  • Ok it's starting to make sense... but still little gray, your talking about `class-inheritance` now... so your saying you have a bunch of `static` or `factory` functions which your `applying` to the map, but before you apply your checking if they already exists? essentially making your `Map` a unique instance built from a bunch of static functions or object ie. `exampleFn()` in your example. –  Jul 11 '17 at 18:18
  • Not really sure where static and factory functions are mentioned here. `exampleFn` isn't static. The general idea is to simplify localization of object metadata without corrupting the object itself. Also, w.r. to your edit suggestion, `Proxy` could be used in my example, but is not necessarily the only proper way, especially since this is an overly-simplified example – nem035 Jul 11 '17 at 18:45
  • It's to simplified doesn't show in real-world use case... so still confused... are you `map.set(someconstructor,'description')` or `map.set(someinstance,'description')`... like can you please provide a real case scenario, the example above you produced doesn't do anything for me differently then what I can already do with `Objects`... –  Jul 11 '17 at 18:59
  • It's not different in what you can do, it's different in **how** you do it. As demonstrated in my answer and prior comments, it is sometimes useful to localize some data that pertains to a certain object without changing the object (this is known as [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns)). There are very limited number of things that you can do with a `Map` that you cannot do with an object, the difference is how you do it. Using a `Map` gives you separation of concerns (modularity) as well as `O(1)` theoretical access (efficiency). – nem035 Jul 11 '17 at 19:03
  • I get you create a collection of objects.... but is that for `constructors` or `instances` and if so... what would be the values if the keys are `objects` and `functions`... some description? and whats the point when I could just use in regular `Array`. –  Jul 11 '17 at 19:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148930/discussion-between-jordan-davis-and-nem035). –  Jul 11 '17 at 19:06
  • @nem035 brilliant example! lets protect this post! –  Jul 11 '17 at 20:28
1

If you write a function that executes an expensive operation to copy / transform / wrap an object or a function, and you expect the function to be called multiple times for the same data, it's typically a performance improvement to do a precursory check in a WeakMap to make sure you haven't already run the expensive operation.

If you have, then you're able to return the result that's already been calculated, which saves a lot of time.

One real-world example is a utility I've published called di-proxy, but to demonstrate my point, the syntax is something like this (in Node.js):

const createInjector = require('di-proxy')
// pass dependency resolver to injector factory 
const inject = createInjector(require)
// wrap IIFE with dependency injector 
inject(({ http, express, 'socket.io': sio }) => {
  const app = express()
  const server = http.Server(app)
  const io = sio(server)
  …
})()

Internally, the createInjector() function will check to make sure it hasn't already generated a wrapper function for require. If it has, it will use the input function as a key to a WeakMap and return the wrapper function it already generated, to save time:

function createInjector (require, noCache = false) {
  …
  // if require function is weakly referenced and memoization is enabled
  if (!noCache && this.has(require)) {
    // return cached injector
    return this.get(require)
  }
  …
  // expensive operation to generate cached injector
  …
  // weakly reference injector with require function as key
  this.set(require, inject)

  // return wrapped function
  return inject
}.bind(new WeakMap())
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • can you write a clear cut example in Javascript not using `node.js` for those who don't use it like myself. –  Jul 11 '17 at 17:35
  • @JordanDavis see the second code block, that covers everything I discussed generally in the first paragraph – Patrick Roberts Jul 11 '17 at 17:36
  • 1
    Your basically checking, if property exists on an object and then using `if/else` statement to preform whatever action, I could preform the same logic using an object and if `(prop in object)`. More importantly what would be the benefit of having a `object` or `function` as a key... and if so what would I put for it's values.... –  Jul 11 '17 at 17:53
  • @JordanDavis I've already addressed your identical comment [in the other answer.](https://stackoverflow.com/questions/45040871/es6-map-object-keys/45041148?noredirect=1#comment77057063_45041199) – Patrick Roberts Jul 11 '17 at 17:57
1

for test cases for example :

const test= new Map();

test.set('isZero', (v) => console.log(v === 0));
test.set('isNumber', (v) => console.log(typeof v === 'number'));

const value = 10;

for(let [key, fn] of test){
  // console.warn(key);
  fn(value);
}
zloctb
  • 10,592
  • 8
  • 70
  • 89