简体   繁体   中英

JavaScript: creating a map that maps multiple keys to a value?

I have a list of keys and a value. For example:

keys = ["keyA", "keyB", "keyC"];

value = 100;

I'm trying to create a function to create a map so that:

map["keyA"]["keyB"]["keyC"] = 100;

I assumed this was the best data structure based on the answer given here :

Anyway, the part that challenges me is that I need a function that will create a map for any number of keys. I've tried doing this in a loop but can't get it to work because I don't know how to access different levels of my map, but it also feels sloppy:

for(var i=0; i<keys.length; i++){
    for(var j=0; j<i; j++){
        maps[keys[0]]...[keys[j]] = {};
        if(j+1 === i){
            maps[keys[0]]...[keys[j]][keys[i]] = value;
        }
    }
}

How can I create my map?

You can try to store a reference to the last created inner object, and go deeper in a loop, in order to make it in a linear time:

 // Input data: var keys = ["keyA", "keyB", "keyC", "keyD", "keyE"]; var value = 100; // Algorithm: var result = {}; var last = result; for (var i = 0; i < keys.length - 1; i++) { last = (last[keys[i]] = {}); // can be change to a two-liner: // last[keys[i]] = {}; // last = last[keys[i]]; } last[keys[keys.length - 1]] = value; // Output: document.body.innerHTML = JSON.stringify(result); document.body.innerHTML += "<br/><br/>" + result["keyA"]["keyB"]["keyC"]["keyD"]["keyE"]; 

Assuming this structure is to be a tree where the nesting has arbitrary depth, first you might benefit from a helper function that lets you access possible non-existent paths safely:

function path(obj, str) {
  return str.split('.').reduce(function (acc, key) {
    return acc instanceof Object ? acc[key] : undefined;
  }, obj);
}

And you also want a way to set such paths neatly:

function setPath(obj, str, val) {
  var path = str.split('.');
  var key  = path.pop();

  var target = path.reduce(function(acc, key) {
    return acc[key] = acc[key] instanceof Object ? acc[key] : {};
  }, obj);

  target[key] = val;
}

Then you have a clean interface for storing and retrieving this data.

map = {};

setPath(map, 'keyA.keyB.keyC', 100);
path(map, 'keyA.keyB.keyC') // 100;
path(map, 'keyA.keyX.keyY') // undefined;

If you prefer, you could have it take arrays of keys instead of dot-notation paths as shown here (just omit the split steps).

Note that if you are never interested in accessing nodes in the tree other than the leaves, or wish to be able to have values for both map.ab and map.a , you can do this much more simply by having a single depth:

map[keys.join('.')] = 100;

And since you've added in a comment that the objective here is actually just to associate a value with a set of keys, and that there is no actual tree structure at all:

function get(map, keys) {
  var key = keys.sort().join('.');
  return map[key];
}

function set(map, keys, val) {
  var key = keys.sort().join('.');
  map[key] = val;
}

If periods are plausible characters in your keys, substitute a different character that you can safely reserve.

If you do not want to maintain a hierarchy of objects, I would suggest you concatenate the keys and store the value with the concatenated string as key.

This assumes you always have the same keys array. If your keys array is supplied externally, you can sort before joining.

See the snippet.

 var keys = ["keyA", "keyB", "keyC", "keyD", "keyE"]; var value = 568; var datastructure = {}; datastructure[keys.join("-")] = value; document.getElementById("output").innerHTML = datastructure[keys.join("-")]; 
 <span id="output"></span> 

Edit 24/12/2022

I have created an ES module for order agnostic multi map. I will explain here how you can set it up for the OP's use case.
https://github.com/martian17/ds-js

First you will want to clone the repository to your project, or copy the code.

$ git clone https://github.com/martian17/ds-js.git

Here is an example use case

// import it to your project
import {OrderAgnosticMultiMap} from "path_to_ds-js/multimap.mjs";

// Instantiate
const map = new OrderAgnosticMultiMap();

// Register values
map.set("keyA", "keyB", "keyC", "content 1");
map.set("keyA", "keyC", "keyC", "content 2");
map.set("keyA", "keyB", "keyB", "content 3");
// The keys can be any object
map.set(map, OrderAgnosticMultiMap, map, window, document, "content 4");


// Get values (keys can be in different orders)
console.log(map.get("keyB", "keyC", "keyA"));
// log: "content 1"
console.log(map.get("keyB", "keyB", "keyC"));
// log: undefined
map.set(document, map, window, OrderAgnosticMultiMap, map);
// log: "content 4"

// Check if a value exists for some keys
console.log(map.has("keyC", "keyC", "keyA"));
// log: true
console.log(map.has("keyA", "keyC", "keyA"));
// log: false

// Loop through values
for(let [tally,value] of map){
    console.log(tally,value);
}
// log:
// Map(3) {"keyA" => 1, "keyB" => 1, "keyC" => 1} 'content 1'
// Map(3) {"keyA" => 1, "keyC" => 2} 'content 2'
// Map(3) {"keyA" => 1, "keyB" => 2} 'content 3'
// Map(3) {map => 2, OrderAgnosticMultiMap => 1, window => 1, document => 1} 'content 4'

// Delete keys
map.delete("keyC", "keyB", "keyA");
map.delete("keyB", "keyB", "keyA");
map.delete("keyC", "keyC", "keyA");
console.log(map.has("keyC", "keyC", "keyA"));
// log: false

Pre-edit

If there is anyone wondering if there is a solution for multi keyed ES6 map, here's my take.
The order does matter though, so map.get(a,b,c) and map.get(c,a,b) will fetch different values.
And you can of course use this as string to object map, so it satisfies the OP's use case as well.

class MultiMap{
    map = new Map;
    own = Symbol();// unique value that doesn't collide
    set(){
        let lst = [...arguments];
        let val = lst.pop();
        let map = this.map;
        for(let k of lst){
            if(!map.has(k))map.set(k,new Map);
            map = map.get(k);
        }
        map.set(this.own,val);// to avoid collision between the same level
        return val;
    }
    get(...lst){
        let map = this.map;
        for(let k of lst){
            if(!map.has(k))return undefined;
            map = map.get(k);
        }
        return map.get(this.own);
    }
    has(...lst){
        let map = this.map;
        for(let k of lst){
            if(!map.has(k))return false;
            map = map.get(k);
        }
        return map.has(this.own);
    }
    delete(...lst){
        let map = this.map;
        let maps = [[null,map]];
        for(let k of lst){
            if(!map.has(k))return false;
            map = map.get(k);
            maps.push([k,map]);
        }
        let ret = map.delete(this.own);
        for(let i = maps.length-1; i > 0; i--){
            if(maps[i][1].size === 0){
                maps[i-1][1].delete(maps[i][0]);
            }else{
                break;
            }
        }
        return ret;
    }
}

Example use case

let a = {a:"a"};
let b = {b:"b"};
let c = {c:"c"};

let mm = new MultiMap;

//basic operations
console.log(mm.set(a,b,c,"abc"));// "abc"
console.log(mm.get(a,b,c));// "abc"
console.log(mm.has(a,b,c));// true
console.log(mm.delete(a,b,c));// true

// overlapping keys can be handled fine as well
mm.set(a,b,"ab");
mm.set(a,"a");
console.log(mm.get(a,b));// "ab"
console.log(mm.get(a));// "a"

For anyone curious about my use case: I was trying to make an event listener wrapper that maps to multiple events internally (mousedown => mousedown, touchstart etc). I needed to cache the arguments when.on() is called so.off() can find the right set of event listeners to remove.

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