【问题标题】:Map using tuples or objects使用元组或对象映射
【发布时间】:2014-03-17 07:13:51
【问题描述】:

我正在尝试使用新的 (ES6) Map 对象来表示属性和值之间的映射。

我有类似以下形式的对象:

 {key1:value1_1,key2:value2_1},..... {key1:value1_N,key2:value2_N}

我想根据 both 它们的 key1 和 key2 值对它们进行分组。

例如,我希望能够按x 和y 对以下内容进行分组:

[{x:3,y:5,z:3},{x:3,y:4,z:4},{x:3,y:4,z:7},{x:3,y:1,z:1},{x:3,y:5,z:4}]

并获得包含以下内容的地图:

{x:3,y:5} ==>  {x:3,y:5,z:3},{x:3,y:5,z:4}
{x:3,y:4} ==>  {x:3,y:4,z:4},{x:3,y:4,z:7}
{x:3,y:1} ==>  {x:3,y:1,z:1}

在 Python 中,我会使用元组作为字典键。 ES6 映射允许任意对象作为键,但使用标准相等算法 (===),因此对象只能通过引用来判断。

如何使用 ES6 映射完成这种分组?或者,如果我忽略了一种优雅的方式,则可以使用普通 JS 对象的解决方案。

我宁愿不使用外部集合库 - 但如果有更好的解决方案,我也有兴趣了解它。

【问题讨论】:

    标签: javascript map equality ecmascript-harmony


    【解决方案1】:

    元组的另一个代码。

    const tuple = (() => {
      const tpls = [];
      return (...args) => {
        let val = tpls.find(tpl => tpl.length === args.length && tpl.every((v,i) => v === args[i]));
        if(val == null) {
          val = Object.freeze([...args]);
          tpls.push(val);
        }
        return val;
      };
    })();
    
    //Usage
    console.assert(tuple(1, 2, 3, foo) === tuple(1, 2, 3, foo));
    //But as bcmpinc says, different objects are not equal.
    console.assert(tuple({}) !== tuple({}));
    
    function foo() {}
    
    const map = new Map();
    map.set(tuple(1, 2, 3, foo), 'abc');
    map.set(tuple(1, 2, 3, foo), 'zzz');
    console.log(map.get(tuple(1, 2, 3, foo)));  // --> 'zzz'
    

    【讨论】:

      【解决方案2】:

      多年过去了,这仍然是 JavaScript 的一个问题。我改进了 Jamesernator 的方法并创建了包 https://www.npmjs.com/package/collections-deep-equal。现在你可以得到你想要的:

      import { MapDeepEqual, SetDeepEqual } from "collections-deep-equal";
      
      const object = { name: "Leandro", age: 29 };
      const deepEqualObject = { name: "Leandro", age: 29 };
      
      const mapDeepEqual = new MapDeepEqual();
      mapDeepEqual.set(object, "value");
      assert(mapDeepEqual.get(object) === "value");
      assert(mapDeepEqual.get(deepEqualObject) === "value");
      
      const setDeepEqual = new SetDeepEqual();
      setDeepEqual.add(object);
      assert(setDeepEqual.has(object));
      assert(setDeepEqual.has(deepEqualObject));
      

      【讨论】:

        【解决方案3】:

        这似乎不太可能。你能做什么?一如既往的可怕。

        let tuple = (function() {
            let map = new Map();
        
            function tuple() {
                let current = map;
                let args = Object.freeze(Array.prototype.slice.call(arguments));
        
                for (let item of args) {
                    if (current.has(item)) {
                        current = current.get(item);
                    } else {
                        let next = new Map();
                        current.set(item, next);
                        current = next;
                    }
                }
        
                if (!current.final) {
                    current.final = args;
                }
        
                return current.final;
            }
        
            return tuple;
        })();
        

        然后瞧。

        let m = new Map();
        m.set(tuple(3, 5), [tuple(3, 5, 3), tuple(3, 5, 4)]);
        m.get(tuple(3, 5)); // [[3, 5, 3], [3, 5, 4]]
        

        【讨论】:

        • 我不确定是否也可以滥用WeakMap 来提高内存效率。可能不会。
        • 你是不是在这里实现了一个元组享元:O。 +1 创造力,但我真的认为这应该在语言层面解决。
        【解决方案4】:

        虽然这个问题已经很老了,但值对象在 JavaScript 中仍然不是现有的东西(所以人们可能仍然感兴趣)所以我决定编写一个简单的库来完成数组作为映射中键的类似行为(repo here: https://github.com/Jamesernator/es6-array-map)。该库的设计基本与 map 的用法相同,只是数组是按元素而不是按身份进行比较的。

        用法:

        var map = new ArrayMap();
        map.set([1,2,3], 12);
        map.get([1,2,3]); // 12
        
        map.set(['cats', 'hats'], {potatoes: 20});
        map.get(['cats', 'hats']); // {potatoes: 20}
        

        警告:但是,该库按身份处理关键元素,因此以下内容不起作用:

        var map = new ArrayMap();
        map.set([{x: 3, y: 5}], {x:3, y:5, z:10});
        map.get([{x: 3, y: 5}]); // undefined as objects within the list are
                                 // treated by identity
        

        但只要你可以将数据序列化为基元数组,你就可以按如下方式使用 ArrayMap:

        var serialize = function(point) {
            return [point.x, point.y];
        };
        var map = new ArrayMap(null, serialize);
        map.set({x: 10, y: 20}, {x: 10, y: 20, z: 30});
        map.get({x: 10, y: 20}); // {x: 10, y: 20, z: 30}
        

        【讨论】:

          【解决方案5】:

          Benjamin 的答案不适用于所有对象,因为它依赖于 JSON.stringify,它不能处理圆形对象,并且可以将不同的对象映射到同一个字符串。 Minitech 的答案可以创建巨大的嵌套映射树,我怀疑这在内存和 CPU 上都是低效的,尤其是对于长元组,因为它必须为元组中的每个元素创建一个映射。

          如果您知道您的元组只包含数字,那么最好的解决方案是使用[x,y].join(',') 作为键。如果您想使用包含任意对象的元组作为键,您仍然可以使用此方法,但必须先将对象映射到唯一标识符。在下面的代码中,我使用 get_object_id 懒惰地生成这些标识符,它将生成的 id 存储在内部映射中。然后我可以通过连接这些 id 来生成元组的键。 (请参阅此答案底部的代码。)

          tuple 方法随后可用于将对象元组散列为可用作映射中键的字符串。这使用对象等价性:

          x={}; y={}; 
          tuple(x,y) == tuple(x,y) // yields true
          tuple(x,x) == tuple(y,y) // yields false
          tuple(x,y) == tuple(y,x) // yields false
          

          如果您确定您的元组将只包含对象(即不为空、数字或字符串),那么您可以在get_object_id 中使用 Wea​​kMap,这样get_object_id 和tuple 就不会泄漏对象作为参数传递给它们。

          var get_object_id = (function() {
            var generated_ids = 1;
            var map = new Map();
            return get_object_id;
            function get_object_id(obj) {
              if (map.has(obj)) {
                return map.get(obj);
              } else {
                var r = generated_ids++;
                map.set(obj, r);
                return r;
              }
            }
          })();
          
          function tuple() {
            return Array.prototype.map.call(arguments, get_object_id).join(',');
          }
          
          // Test
          var data = [{x:3,y:5,z:3},{x:3,y:4,z:4},{x:3,y:4,z:7},
                      {x:3,y:1,z:1},{x:3,y:5,z:4}];
          var map = new Map();
          for (var i=0; i<data.length; i++) {
            var p = data[i];
            var t = tuple(p.x,p.y);
            if (!map.has(t)) map.set(t,[]);
            map.get(t).push(p);
          }
          
          function test(p) {
            document.writeln((JSON.stringify(p)+' ==> ' + 
              JSON.stringify(map.get(tuple(p.x,p.y)))).replace(/"/g,''));
          }
          
          document.writeln('<pre>');
          test({x:3,y:5});
          test({x:3,y:4});
          test({x:3,y:1});
          document.writeln('</pre>');

          【讨论】:

          • 请解释为什么你认为我关于弱图的那句话是错误的。此外,如果您的答案可以在没有 JSON.stringify 的情况下使用,请更新您的答案以说明如何操作。
          • 使用 Wea​​kMap 可确保元组中使用的对象仍然有资格进行垃圾回收。 IE。如果 WeakMap 是唯一引用对象的东西,垃圾收集器可以销毁该对象(并将其从 WeakMap 中删除)。从而避免内存泄漏。
          • 对象在 WeakMap 中被用作 keys,所以如果它们符合垃圾回收的条件就可以了。该映射用于扩展具有唯一标识符的对象,而无需实际更改对象。 -- 要将答案中的 JSON.stringify 替换为另一个哈希函数,您还需要创建这样的其他哈希函数,这很困难。我的tuple() 是这样的其他哈希函数,具有与 JSON.stringify 不同(更好)的属性。除了散列函数,我们的答案是一样的:将元组映射到可以用作键的东西。
          • 不,它使用 Wea​​kMaps 正是为了它们的设计目的。是的,我知道对于您的问题,您应该使用 .join(',') 作为哈希函数。尽管您的问题的读者可能有不同的要求。您的解决方案不适用于循环对象或 JSON.stringify 映射到同一字符串的对象。而且对于复杂的对象效率很低。
          【解决方案6】:

          好的,我现在已经在esdiscuss 上提出了这个问题,我得到了 Mozilla 的Jason Orendorff 的答复:

          1. 这是 ES6 映射的问题。
          2. 解决方案将以 ES7 value objects 的形式出现,用于键而不是对象。
          3. 之前考虑过让人们指定.equals 和.hashCode,但它被拒绝以支持值对象。 (我认为有充分的理由)。
          4. 目前唯一的解决方案是推出您自己的收藏。

          Bradley 在 ESDiscuss 线程上提供了一个基本的此类集合(概念,不要在生产代码中使用),可能看起来像这样:

          function HashMap(hash) {
            var map = new Map;
            var _set = map.set;
            var _get = map.get;
            var _has = map.has;
            var _delete = map.delete;
            map.set = function (k,v) {
              return _set.call(map, hash(k), v);
            }
            map.get = function (k) {
              return _get.call(map, hash(k));
            }
            map.has = function (k) {
              return _has.call(map, hash(k));
            }
            map.delete = function (k) {
              return _delete.call(map, hash(k));
            }
            return map;
          }
          
          function TupleMap() {
            return new HashMap(function (tuple) {
              var keys = Object.keys(tuple).sort();
              return keys.map(function (tupleKey) { // hash based on JSON stringification
                         return JSON.stringify(tupleKey) + JSON.stringify(tuple[tupleKey]);
              }).join('\n');
              return hashed;
            });
          }
          

          更好的解决方案是使用类似MontageJS/Collections 的东西,它允许指定散列/等于函数。

          您可以查看 API 文档here。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-07-09
            • 1970-01-01
            • 2020-03-14
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-12-22
            相关资源
            最近更新 更多