JavaScript - 将值映射到键(反向对象映射)

11

我想要反转一个对象的映射(该对象可能具有重复的值)。 例如:

const city2country = {
    'Amsterdam': 'Netherlands',
    'Rotterdam': 'Netherlands',
    'Paris': 'France'
};

reverseMapping(city2country)应该输出:

{
    'Netherlands': ['Amsterdam', 'Rotterdam'],
    'France': ['Paris']
}

我想了一个天真的解决方案:

const reverseMapping = (obj) => {
    const reversed = {};
    Object.keys(obj).forEach((key) => {
        reversed[obj[key]] = reversed[obj[key]] || [];
        reversed[obj[key]].push(key);
    });
    return reversed;
};

但我相信一定有更加简洁、短小的方式,最好还能够原型化,这样我就可以直接执行:

const country2cities = city2country.reverse();

6
据我所知,目前没有内置的功能可以做到这一点。你的代码看起来很好。 - Barmar
4
最好是原型化,你不想添加到Object.prototype。 - Jaromanda X
2
@SagarV OP正在寻找一行代码解决方案。唯一的方法是使用库,因为没有内置的功能可以实现它。 - Barmar
1
我更喜欢代码的可读性,而不是“更整洁、更短”的方式,所以你的代码很好。你甚至可以在forEach内部加上var objKey=obj[key];来使它更易读。你已经有一个可用的代码了,不要浪费时间试图压缩它(除非这是你的实际目标)。 - diynevala
1
普遍观点。在SO上搜索。它已经被反复讨论过了。 - Jaromanda X
显示剩余3条评论
7个回答

11

JavaScript中没有这样的内置函数。你的代码看起来不错,但考虑到这里可能会出现很多边界情况,我建议使用lodash的invertBy函数,它正好实现了你描述的功能。

示例

var object = { 'a': 1, 'b': 2, 'c': 1 };

_.invertBy(object);
// => { '1': ['a', 'c'], '2': ['b'] }

3
有人真的因为需要第三方库而对这个答案进行了负评吗?这是唯一一个真正满足问题提出者需求的答案。 - Barmar
谢谢,我们目前不使用lodash,为这么小的一个方法添加lodash似乎有些过度。我也不明白为什么会被踩。 - Daniel
3
@Daniel lodash是模块化的,这意味着你只能安装这一个函数并使用。在大小方面与从这里复制/粘贴函数没有太大区别,但对于维护来说更好。仅是我的个人见解。 - Lazar Ljubenović
1
@LazarLjubenović 实际上看起来lodash代码与我的代码非常相似,除了hasOwnProperty防御,无论如何还是谢谢! - Daniel

10
你可以使用 Object.assign,同时尊重插入值的给定数组。

const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };
const reverseMapping = o => Object.keys(o).reduce((r, k) =>
        Object.assign(r, { [o[k]]: (r[o[k]] || []).concat(k) }), {})

console.log(reverseMapping(city2country));


1
谢谢!这绝对是我所寻找的最接近的东西。 - Daniel

1

你可以使用 reduce 来节省声明行 reduce

滥用 && 来先检查 map[object[key]] 是否已定义,然后再使用 Array.concat

这样更短,但更简单吗?可能不是,但有点有趣 ;)

const reverseMapping = (object) => 
    Object.keys(object).reduce((map, key) => {
        map[object[key]] = map[object[key]] && map[object[key]].concat(key) || [key]
     return map;
    }, {});

为什么需要防止重复?对象键(Object keys)保证是唯一的。 - Barmar
哦,看来我错了。我一定是使用了 concat(x, y) 而不是 concat(x)。更新我的代码,因为我宁愿不使用 Set。 - user2340824

0
受到其他答案的启发,这里有一个简短的可重复使用函数:
/**
 * Take an object with key->value mapping and return
 * an object with the inverse value->keys[] mapping.
 */
function invertObj(obj) {
  return Object.entries(obj)
    .reduce((result, [key, value]) => {
      result[value] = (result[value] || []).concat(key);
      return result;
    }, {});
}

使用问题中的示例:

invertObj({
    'Amsterdam': 'Netherlands',
    'Rotterdam': 'Netherlands',
    'Paris': 'France'
});
// => {
//   "Netherlands": [ "Amsterdam", "Rotterdam" ],
//   "France": [ "Paris" ],
// }

0

您可以尝试从当前对象获取值数组和键数组,并设置一个新对象来保存结果。然后,当您循环遍历值数组时 -

  • 如果对象已经将此值作为键,例如 Netherlands,则创建一个新数组,获取已存在的值(例如:Rotterdam),并将其与新值(Amsterdam)添加到数组中,并将此数组设置为 Netherlands 键的新值。
  • 如果当前值不存在于对象中,则将其设置为新字符串,例如: France 是键,Paris 是值。

代码 -

const city2country = {
    'Amsterdam': 'Netherlands',
    'Rotterdam': 'Netherlands',
    'Paris': 'France',
};

function reverseMapping(obj) {
  let values = Object.values(obj);
  let keys = Object.keys(obj);
  let result = {}
  values.forEach((value, index) => {
    if(!result.hasOwnProperty(value)) {
      // create new entry
      result[value] = keys[index];
    }
    else {
      // duplicate property, create array
      let temp = [];
      // get first value
      temp.push(result[value]);
      // add second value
      temp.push(keys[index]);
      // set value 
      result[value] = temp;
    }
  });
  
  console.log(result);
  return result;
}

reverseMapping(city2country)

这里的好处是 - 它会根据您当前对象的结构进行调整 - Netherlands 作为重复值,在新对象中得到一个数组作为其值,而 France 则以字符串值 Paris 作为其属性。当然,这很容易改变。
注意 - Object.values() 可能不支持旧浏览器。

2
他正在寻找比他现有的代码更简单的东西。而你的函数长度是他的三倍长。 - Barmar

0
你可以先使用类似以下的代码来去除重复项:

function removeDuplicates(arr, key) {
   if (!(arr instanceof Array) || key && typeof key !== 'string') {
    return false;
   }

  if (key && typeof key === 'string') {
    return arr.filter((obj, index, arr) => {
        return arr.map(mapObj => mapObj[key]).indexOf(obj[key]) === index;
    });

  } else {
    return arr.filter(function(item, index, arr) {
        return arr.indexOf(item) == index;
    });
  }
} 

然后使用这个来将其反转:

function reverseMapping(obj){
  var ret = {};
  for(var key in obj){
  ret[obj[key]] = key;
  }
    return ret;
  }

reverseMapping(city2country) 只将国家映射到一个城市,而不是城市数组。您如何使用您的答案来获得他想要的结果? - Barmar

0

@Nina Scholz的答案对于这个问题非常有效。:thumbsup:

但是,如果您不需要保留荷兰键("Netherlands": ["Amsterdam", "Rotterdam"])的两个值,则以下代码会更短,更简单易读:

const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };

console.log(
  Object.entries(city2country).reduce((obj, item) => (obj[item[1]] = item[0]) && obj, {})
);
// outputs `{Netherlands: "Rotterdam", France: "Paris"}`

实际上是你破坏了她的代码。你丢失了“阿姆斯特丹”的值... 再次检查SO的问题;以及Nina的答案代码输出。 - Kamafeather
你说得对,我没有注意到那个部分!我会把答案留在这里,但是会提供更多细节来说明正确使用的情况。这可能会帮助一些略有不同情况的人。 :) - antoni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接