Python has setdefault
which lets you set a default value for a dictionary entry.
There’s a good example here which shows making the default an array so you can do
dict[id].append(elem)
without having to check if dict[id]
exists.
In JavaScript I could do this
const dict = {};
(dict[id] = dict[id] ?? []).push(elem)
But I was wondering if I could somehow implement similar functionality to python’s setdefault
I tried this
function makeObjectWithDefault(defaultFactory) {
const obj = {};
const handler = {
get(target, prop, receiver) {
let v = target[prop];
if (v === undefined) {
v = defaultFactory();
target[prop] = v;
}
return v;
},
}
return new Proxy(obj, handler)
}
It partly worked
const dict = makeObjectWithDefault(() => []);
dict['abc'].push(123);
dict.def.push(456);
But then unfortunately it adds extra properties
console.log(JSON.stringify(dict, null, 2));
prints
{
"abc": [
123
],
"def": [
456
],
"toJSON": [] // !!! <- because it looked for a `toJSON` function
}
Because JSON.stringify
looked for toJSON
function on the object it ended up adding an array for that entry.
Of course I could check if the prop
equals toJSON
but that’s not really valid since I should be able to do dict['toJSON'].push(789)
if I wanted to. And, I’d have to figure out which other functions try to access various properties (eg, toString
)
Is it possible to implement python’s setdefault
for JavaScript objects?
Alternatives:
-
a helper
Of course I could write a helper
function accessWithDefaultArray(dict, id) { return (dict[id] = dict[id] ?? []); } accessWithDefaultArray(dict, 'abc').push(123); accessWithDefaultArray(dict, 'def').push(456);
But it’s not very pretty
-
I could write a class but it would require not using standard syntax
-
A
Map
might work as a substitute since it usesget
andset
and so won’t
suffer from confusion between access of methods and access of elements.
In that case a simple class would workclass MapWithDefault extends Map { constructor(defaultFactory, iterable) { super(iterable); this.defaultFactory = defaultFactory; } get(key) { let v = super.get(key); if (!v) { v = this.defaultFactory(); super.set(key, v); } return v; } } { const d = new MapWithDefault(() => []); d.get('abc').push(123); d.get('def').push(456); d.get('def').push(789); console.log(JSON.stringify(Object.fromEntries(d.entries()), null, 2)); }
It’s not as nice as
d['abc'].push(123)
andd.def.push(456)
and it’s not as directly
usable asObject
in various places.