49

In Python you can have a defaultdict(int) which stores int as values. And if you try to do a get on a key which is not present in the dictionary you get the default value, 0 in case of int.

How can you do something similar in Javascript/JQuery?

Rodrigue
  • 3,617
  • 2
  • 37
  • 49
user462455
  • 12,838
  • 18
  • 65
  • 96
  • possible duplicate of [Set undefined javascript property before read](http://stackoverflow.com/questions/11503666/set-undefined-javascript-property-before-read) – SheetJS Oct 02 '13 at 02:48

8 Answers8

66

You can build one using a JavaScript Proxy

var defaultDict = new Proxy({}, {
  get: (target, name) => name in target ? target[name] : 0
})

This lets you use the same syntax as normal objects when accessing properties.

defaultDict.a = 1
console.log(defaultDict.a) // 1
console.log(defaultDict.b) // 0
defaultDict['c'] += 20
console.log(defaultDict.c) // 20

To clean it up a bit, you can wrap this in a constructor function, or perhaps use the class syntax.

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

EDIT: The above implementation only works well with primitives. It should handle objects too by taking a constructor function for the default value. Here is an implementation that should work with primitives and constructor functions alike.

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) // []
EoghanM
  • 25,161
  • 23
  • 90
  • 123
Andy Carlson
  • 3,633
  • 24
  • 43
  • 6
    I like this answer, but if defaultVal is an *array* (or anything modified by reference?) may produce unexpected results. I used this variation: `dd = new Proxy({}, { get: (target, name) => name in target ? target[name] : (target[name]=[]) })` , or this variation : `class DefaultDict2 { constructor(defaultValConstructor) { return new Proxy({}, { get: (target, name) => name in target ? target[name] : new defaultValConstructor() }) } }` – Nate Anderson May 06 '18 at 15:33
  • 1
    I like this answer too, could you extend it to show nested defaultdicts? – Alex Lenail Oct 29 '18 at 01:59
  • 1
    The last version makes it so there's a `toJSON` key in the `counts` and `lists` objects. Is there a way to fix that? – Dave Johansen Dec 02 '19 at 06:18
  • 1
    Great answer! An additional thing to note is that even if you only read from a property, it will create the default element, so if you do `console.log(dict.a)` and then use `Object.keys(dict)`, `a` will still show up even though you never assigned it a value. – Eric Wiener Apr 27 '20 at 14:22
  • `name in target ? target[name] : defaultVal` looks like an error. i think it should be `name in target ? target[name] : target[name] = defaultVal` – Jayen Aug 16 '21 at 05:00
19

Check out 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

I don't think there is the equivalent but you can always write your own. The equivalent of a dictionary in javascript would be an object so you can write it like so

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

Then call it like so

var myDict = new defaultDict();
myDict[1] = 2;
myDict.get(1);
DGS
  • 6,015
  • 1
  • 21
  • 37
3

A quick dirty hack can be constructed using 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]
        }
    })
}

So the following code:

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)
JP Ventura
  • 5,564
  • 6
  • 52
  • 69
2

Proxies definitely make the syntax most Python-like, and there's a library called defaultdict2 that offers what seems like a pretty crisp and thorough proxy-based implementation that supports nested/recursive defaultdicts, something I really value and am missing in the other answers so far in this thread.

That said, I tend to prefer keeping JS a bit more "vanilla"/"native" using a function-based approach like this proof-of-concept:

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

The constructor of DefaultMap accepts a function that returns a default value for leaf nodes. The basic operations for the structure are put and get, the latter of which is self-explanatory. put generates a chain of nested keys and returns a pair of functions that let you mutate or set the leaf node at the end of these keys. Accessing .root gives you the underlying Map structure.

Feel free to leave a comment if I've overlooked any bugs or miss useful features and I'll toss it in.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
1

Inspired by @Andy Carlson's answer, here's an implementation that works in a slightly more Pythonic way:

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;
                    }
                },
            }
        );
    }
}

Basically, it also lets you retrieve all the gotten and set values of the "target", similar to how collections.defaultdict works in Python. This allows us to do things like:

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}
Ulf Aslak
  • 7,876
  • 4
  • 34
  • 56
0

To add to Andy Carlson's answer

If you default dict an array, you'll get a toJSON field in the resulting object. You can get rid of it by deconstructing to a new object.

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

The original answer does not seem to work on the nested cases. I made some modifications to make it work:

  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;

The difference is that if defaultInit is an object, we need to return a deep copy of the object, instead of the original one.