在JavaScript中,defaultdict的等价物是什么?

49
在Python中,你可以使用defaultdict(int)来存储int作为值。如果你尝试对字典中不存在的键进行get操作,你将得到默认值,在int的情况下是0
那么在Javascript/JQuery中如何实现类似的功能呢?

可能是在读取之前设置未定义的JavaScript属性的重复问题。 - SheetJS
8个回答

66
你可以使用JavaScript的Proxy来构建一个。
var defaultDict = new Proxy({}, {
  get: (target, name) => name in target ? target[name] : 0
})

这样可以让您在访问属性时使用与普通对象相同的语法。
defaultDict.a = 1
console.log(defaultDict.a) // 1
console.log(defaultDict.b) // 0
defaultDict['c'] += 20
console.log(defaultDict.c) // 20

要稍微整理一下,你可以将其包装在构造函数中,或者使用类语法。

class DefaultDict {
  constructor(defaultVal) {
    return new Proxy({}, {
      get: (target, name) => name in target ? target[name] : defaultVal
    })
  }
}

const counts = new DefaultDict(0)
console.log(counts.c) // 0

编辑:上述实现仅适用于基本类型。它还应该通过接受默认值的构造函数来处理对象。这里是一个可以同时适用于基本类型和构造函数的实现。

class DefaultDict {
  constructor(defaultInit) {
    return new Proxy({}, {
      get: (target, name) => name in target ?
        target[name] :
        (target[name] = typeof defaultInit === 'function' ?
          new defaultInit().valueOf() :
          defaultInit)
    })
  }
}


const counts = new DefaultDict(Number)
counts.c++
console.log(counts.c) // 1

const lists = new DefaultDict(Array)
lists.men.push('bob')
lists.women.push('alice')
console.log(lists.men) // ['bob']
console.log(lists.women) // ['alice']
console.log(lists.nonbinary) // []

6
我喜欢这个答案,但是如果defaultVal是一个数组(或者任何被引用修改的东西?)可能会产生意想不到的结果。我使用了这个变体:dd = new Proxy({}, { get: (target, name) => name in target ? target[name] : (target[name]=[]) }) 或者这个变体: class DefaultDict2 { constructor(defaultValConstructor) { return new Proxy({}, { get: (target, name) => name in target ? target[name] : new defaultValConstructor() }) } } - Nate Anderson
1
我也喜欢这个答案,你能否扩展一下,以显示嵌套的defaultdicts呢? - Alex Lenail
1
最新版本使得countslists对象中有一个toJSON键。有没有办法解决这个问题? - Dave Johansen
1
很棒的答案!另外需要注意的是,即使你只是从一个属性中读取数据,它也会创建默认元素。因此,如果你执行 console.log(dict.a) 然后使用 Object.keys(dict)a 仍然会出现,即使你从未给它赋值。 - Eric Wiener
name in target ? target[name] : defaultVal looks like an error. i think it should be name in target ? target[name] : target[name] = defaultVal - Jayen

19

看一下pycollections.js

var collections = require('pycollections');

var dd = new collections.DefaultDict(function(){return 0});
console.log(dd.get('missing'));  // 0

dd.setOneNewValue(987, function(currentValue) {
  return currentValue + 1;
});

console.log(dd.items()); // [[987, 1], ['missing', 0]]

4

我认为并没有等价的东西,但您总是可以自己编写。在JavaScript中,字典的等效物是对象,所以您可以像这样编写:

function defaultDict() {
    this.get = function (key) {
        if (this.hasOwnProperty(key)) {
            return key;
        } else {
            return 0;
        }
    }
}

然后像这样调用它。
var myDict = new defaultDict();
myDict[1] = 2;
myDict.get(1);

5
如果你想将默认值设为集合(例如列表的映射),则这种方法行不通。 - Elliott Slaughter

3
可以使用Proxy来快速粗略地构建黑客方法。
function dict(factory, origin) {
    return new Proxy({ ...origin }, {
        get(dict, key) {
            // Ensure that "missed" keys are set into
            // The dictionary with default values
            if (!dict.hasOwnProperty(key)) {
                dict[key] = factory()
            }

            return dict[key]
        }
    })
}

因此,下面的代码:

n = dict(Number, [[0, 1], [1, 2], [2, 4]])

// Zero is the default value mapped into 3
assert(n[3] == 0)

// The key must be present after calling factory
assert(Object.keys(n).length == 4)

2

代理绝对使Python语法最接近,有一个名为defaultdict2的库提供了一种似乎相当清晰和全面的基于代理的实现,支持嵌套/递归字典,这是我在此线程中其他答案中追求并且缺失的东西。

话虽如此,我倾向于保持JS更加“原生”,使用类似这个概念证明的基于函数的方法:

class DefaultMap {
  constructor(defaultFn) {
    this.defaultFn = defaultFn;
    this.root = new Map();
  }
  
  put(...keys) {
    let map = this.root;
    
    for (const key of keys.slice(0, -1)) {
      map.has(key) || map.set(key, new Map());
      map = map.get(key);
    }

    const key = keys[keys.length-1];
    map.has(key) || map.set(key, this.defaultFn());
    return {
      set: setterFn => map.set(key, setterFn(map.get(key))),
      mutate: mutationFn => mutationFn(map.get(key)),
    };
  }
  
  get(...keys) {
    let map = this.root;

    for (const key of keys) {
      map = map?.get(key);
    }

    return map;
  }
}

// Try it:
const dm = new DefaultMap(() => []);
dm.put("foo").mutate(v => v.push(1, 2, 3));
dm.put("foo").mutate(v => v.push(4, 5));
console.log(dm.get("foo")); // [1, 2, 3, 4, 5]
dm.put("bar", "baz").mutate(v => v.push("a", "b"));
console.log(dm.get("bar", "baz")); // ["a", "b"]
dm.put("bar", "baz").set(v => 42);
console.log(dm.get("bar", "baz")); // 42
dm.put("bar", "baz").set(v => v + 1);
console.log(dm.get("bar", "baz")); // 43

构造函数DefaultMap接受一个返回叶节点默认值的函数。该结构的基本操作为putget,后者不需要解释。put生成一系列嵌套的键,并返回一对函数,让您可以更改或设置这些键末尾的叶节点。访问.root可获取底层Map结构。
如果我忽略了任何错误或遗漏了有用的功能,请随时发表评论,我会加入它们。

1
受@Andy Carlson答案的启发,这里提供一个更Pythonic的实现方式:
class DefaultDict {
    constructor(defaultVal) {
        return new Proxy(
            {},
            {
                get: (target, name) => {
                    if (name == '__dict__') {
                        return target;
                    } else if (name in target) {
                        return target[name];
                    } else {
                        target[name] = defaultVal;
                        return defaultVal;
                    }
                },
            }
        );
    }
}

基本上,它还允许您检索“目标”的所有获取和设置值,类似于Python中的collections.defaultdict的工作方式。这使我们可以做到以下事情:

const myDict = new DefaultDict(0);

myDict['a'] += 1;
myDict['b'] += 2;
myDict['c'] += 3;

myDict['whatever'];

console.log(myDict.__dict__);
// {'a': 1, 'b': 2, 'c': 3, 'whatever': 0}

0

原来的答案似乎在嵌套情况下不起作用。我做了一些修改使其正常工作:

  class DefaultDict {
    constructor(defaultInit) {
      this.original = defaultInit;
      return new Proxy({}, {
        get: function (target, name) {
          if (name in target) {
            return target[name];
          } else {
            if (typeof defaultInit === "function") {
              target[name] = new defaultInit().valueOf();
            } else if (typeof defaultInit === "object") {
              if (typeof defaultInit.original !== "undefined") {
                target[name] = new DefaultDict(defaultInit.original);
              } else {
                target[name] = JSON.parse(JSON.stringify(defaultInit));
              }
            } else {
              target[name] = defaultInit;
            }
            return target[name];
          }
        }
      });
    }
  }

  var a = new DefaultDict(Array);
  a["banana"].push("ya");
  var b = new DefaultDict(new DefaultDict(Array));
  b["orange"]["apple"].push("yo");
  var c = new DefaultDict(Number);
  c["banana"] = 1;
  var d = new DefaultDict([2]);
  d["banana"].push(1);
  var e = new DefaultDict(new DefaultDict(2));
  e["orange"]["apple"] = 3;
  var f = new DefaultDict(1);
  f["banana"] = 2;

区别在于,如果defaultInit是一个对象,我们需要返回该对象的深拷贝,而不是原始对象。


0

补充Andy Carlson的回答:

如果你默认字典一个数组,你会在结果对象中得到一个toJSON字段。你可以通过解构到一个新对象来摆脱它。

const dd = new DefaultDict(Array);
//...populate the dict
return {...dd};

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