简体   繁体   中英

JavaScript: reverse mapping from an object id to that object

I have a class Rectangle which has a prop called id that indicates the uniqueness of that instance of Rectangle

class Rectangle {
  constructor(desc) {
    this.desc = desc
    this.id = Symbol()
  }
}

Later I need to put the id to some other data structure and I need a quick way to look up that Rectangle instance using the id

To achieve this I need to manually create a map eg

const r1 = new Rectangle('first')
const r2 = new Rectangle('second')
const r3 = new Rectangle('third')

const map = {
  [r1.id]: r1,
  [r2.id]: r2,
  [r3.id]: r3,
}

So I can do to get the reference of r1 if I need to

map[r1.id]

I wonder if there is a programmatic way to achieve this reverse mapping where id is the key and the Rectangle instance is the value?

Also, I would appreciate it if someone can suggest a better name for the map - right now the name map is not really descriptive.

Lastly, I wonder if we can get rid of id and just use the reference of the Rectangle instance? I know the answer is probably "it depends" but I think most of the time we would need an id for the instances or objects. I wonder if in what circumstances using only references is not going to cut it?

you can collect the rectangles in an array and use reduce:

const r1 = new Rectangle('first')
const r2 = new Rectangle('second')
const r3 = new Rectangle('third')

const input = [r1, r2, r3];
const map = input.reduce((output, rect) => ({...output, [rect.id]: rect}), {})
console.log(map)

If you are open to integrating external libraries into the project, one possibility is to use Lodash's keyBy method. It takes an array of objects as the first input and a function to calculate the keys of the resulting object.

If I take your example, this would be the resulting code:

import { keyBy } from 'lodash';

const r1 = new Rectangle('first');
const r2 = new Rectangle('second');
const r3 = new Rectangle('third');

const map = keyBy([r1, r2, r3], (rectangle) => rectangle.id);

You will still need to build the array itself, but this would have to be done in any case. If you want a more complex data structure, you can create a class with an internal array representation of the data, and some methods to access it. Something like this (I'm using typescript, javascript would be similar but without the typing, plus I'm not testing whether this would work just with copy and paste, there may be some minor adjustments needed):

import { keyBy, filter } from 'lodash';

class RectangleMap {
  private recArray: Rectangle[];

  constructor() {
    this.recArray = [];
  }

  addRectangle(r: Rectangle): void {
    this.recArray.push(r);
  }

  removeRectangle(r: Rectangle): void {
    this.recArray = filter(this.recArray, (rec) => rec.id !== r.id);
  }

  getMap(key: string = 'id'): Record<string, Rectangle> {
    return keyBy(this.recArray, rec => rec[key]);
  }
}

const myRecMap = new RectangleMap();

myRecMap.addRectangle(new Rectangle('first'));
myRecMap.addRectangle(new Rectangle('second'));
myRecMap.addRectangle(new Rectangle('third'));

console.log(myRecMap.getMap());

In general, I always recommend Lodash for such array / object operations. They have very good utility functions.

I wonder if there is a programmatic way to achieve this reverse mapping where id is the key and the Rectangle instance is the value?

If the data structure you need is a map with id as key, there is no already built way to do that without explicit key-value insertion. You probably have 2 options:

  • A little improvement over how you create the map:

     class Rectangle { constructor(desc) {...} getAsMapEntry() { return [this.id, this] } } const map = new Map([ r1.getAsMapEntry(), r2.getAsMapEntry(), r3.getAsMapEntry() ]);

    Depending on your situation, you should decide whether keeping the getAsMapEntry method inside the class Rectangle or as a utility method. For simplicity I showed the first approach, but I would probably opt for the second one. Moreover, I strongly suggest you to use Map class here, for many reasons .

  • You can create a new data structure that accepts Rectangle s (or even better, all kinds of objects with the id property) and internally has a map. This new class is like an adapter to make the operations you need more readable at least. Do not extend the Map class because this inheritance would break the Liskov Principle , since you would have to change drastically the preconditions of the set method. The easier way would be proceeding by composition:

     class EntityContainer { // accept an array of Rectangle (or objects having the id property) constructor(entities) { this._internalMap = new Map(entities.map(e => [e.id, e])); } _assertIsEntity(entity) { if (.entity.id) throw "not an entity" } insertOrUpdate(entity) { this;_assertIsEntity(entity). this._internalMap.set(entity,id; entity). } deleteByEntity(entity) { this;_assertIsEntity(entity). this.deleteById(entity;id). } deleteById(id) { this._internalMap.delete(id) } getById(id) { this._internalMap;get(id); } }

The benefit with this approach is that you can hide the id -based logic inside the EntityContainer class, meaning that you insert entities, not id-entity pairs for example. Same goes for deletion with deleteByEntity , if you need it of course. You can validate the entities too, and you generally have more control over the operations you do, at the expense of a new class to maintain however. The drawback comes out when the method you write simply project the _internalMap method, see getById . This is not so beautiful, but in my opinion acceptable if you have to do that for just a few methods.

Also, I would appreciate it if someone can suggest a better name for the map - right now the name map is not really descriptive.

That is something you have to decide, depending on its purpose, of course.

Lastly, I wonder if we can get rid of id and just use the reference of the Rectangle instance? I know the answer is probably "it depends" but I think most of the time we would need an id for the instances or objects. I wonder if in what circumstances using only references is not going to cut it?

Again, you did not provide us with enough information to give you an advice on whether it is more appropriate to use a reference or the id property (just to make it clear, there is no rule, as always). Based on your comment " it is part of a larger system ", the id is probably a good tradeoff because it is the easier way to create relationships between multiple components, especially when an external data store is involved for instance.

I think it would be better to create 2 static properties in the Rectangle class and use them to keep track of the instances


class Rectangle {
  constructor(desc) {
    this.desc = desc;
    this.id = Symbol();
    Rectangle.instances[this.id] = this;
  }

  static instances = {};
  static getInstance = (id) => {
    return this.instances[id];
  }
}

let r1 = new Rectangle('one');
let r2 = new Rectangle('two');

console.log(Rectangle.getInstance(r1.id)); // gives the r1 instance

where id is the key and the Rectangle instance is the value?

It was strangely worded(so im not sure if I understand what you mean, but it seems like using a static is best for storing created Rectangles in a data structure similar to map )

PS : if u open up inspect element, you would see the id s of these objects(The display doesn't show it though)

 class Rectangle { constructor(desc) { this.desc = desc this.id = Symbol(desc) Rectangle.list[this.id]=this } static list={} //this is an your map(kinda) static getRectangleById=(id)=>Rectangle.list[id]||null //getRectangleById would return null if id is invalid } let r1=new Rectangle("first") let r2=new Rectangle("second") let r3=new Rectangle("third") //example usage //btw these objects do have 2 keys, just that THIS DOESNT LOG SYMBOLS let locatedRect=Rectangle.getRectangleById(r1.id) //null if invalid console.log(locatedRect) let sameRect=Rectangle.list[r1.id] //undefined if invalid console.log(sameRect) console.log(locatedRect==sameRect) console.log(Object.keys(sameRect)) //proof that stack snippets don't show symbols >:{

const r1 = new Rectangle('first')
const r2 = new Rectangle('second')
const r3 = new Rectangle('third')

const rectangles = [r1, r2, r3];
const map = rectangles.reduce((output, rectangle) => ({...output, [rectangle.id]: rectangle}), {})
console.log(map)

By reducing over the rectangles in the map one by one we can basically insert the rectangles using id as keys by using [rectangle.id]: rectangle .

Running example

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM