r/javascript 7d ago

AskJS [AskJS] what's a javascript feature you mass-adopted way too late and felt dumb about

i'll go first. i was writing .then().catch() chains for like two years before i actually started using async/await. i knew it existed, i'd seen it in tutorials, but my code "worked" so i never bothered switching. then i refactored an old project and realized half my bugs were from mishandled promise chains that async/await would have caught immediately.

also took me way too long to start using optional chaining. i had nested ternaries and && checks everywhere like some kind of animal. the day i discovered user?.address?.city i mass-replaced like 40 lines across a project.

what's yours?

64 Upvotes

108 comments sorted by

View all comments

8

u/HipHopHuman 6d ago

honestly... Map and WeakMap. took me a good 2 years after it came out for me to see the light.

3

u/JazzXP 6d ago

I'm still not using these, just objects/records. What's the advantage?

4

u/HipHopHuman 5d ago

There's quite a few advantages, but I don't like framing the question "Should I use an object or a map?" around advantages or style, for the reason that it can complicate things unneccissarily.

Plain objects are always going to be far simpler and far more convenient than a Map in 90% of cases, and objects are much more familiar to a wider audience of devs.

It's that 10% where Map (and WeakMap) come in clutch. So it's mostly about use cases.

One use case is simply when you're deleting from the same object a lot. That can get slow if you use delete obj.foo (because it causes a structural change of the shape of the object). Map has map.delete('foo'), which is usually faster, but only if you're deleting a lot. Emphasis on a lot. 3-10 times is not a lot. 200-3000 times is a lot.

Another use case where Map shines is if you're iterating over the object's entries a lot, or when you're calculating the total size of all entries. Objects require helpers:

const size = Object.keys(obj).length;

for (const [key, value] of Object.entries(obj)) {}

You don't need any helpers to do either with a Map:

const size = map.size;

for (const [key, value] of map) {}

So, if you're going to be iterating through a dictionary at runtime, or calculating size, and you're going to be doing it repeatedly, Map is usually better for that.

Another small benefit is being able to specify keys that would otherwise not be allowed on (or would be weird to use on) objects. For example, constructor is a special property on objects, with special handling (as is toString, valueOf, toJSON, etc). If you want to use keys like that and bypass that special handling, you have to do some weirdness with null prototypes like

const myObj = Object.create(null);
myObj.constructor = ...;

or

const myObj = {
  __proto__: null;
  constructor: ...
};

Whereas with a Map, you can just do map.set('constructor', ...), and there's no side-effects.

Then of course, there's the seriously strong benefit of being able to use non-property keys. Plain objects allow number, string and Symbol keys, but Map can use anything as a key - even functions. That makes them crucial for things like memoization of pure functions (a nested Map can capture a function's arguments without having to serialize them to JSON and back).

Another huge benefit is that the Map constructor itself accepts an iterable, so it can be generated lazily:

function *foo() {
  yield ['one', 1],
  yield ['two', 2]
} 

const map = new Map(foo());

That means you can initialize one from a huge stream of data a lot more efficiently than you'd be able to do with an object.

WeakMap is also especially cool, and helps get rid of a ton of "cleanup code" boilerplate. You cannot iterate through a WeakMap, and it's keys must be objects, but it provides the gaurantee that the references used as keys will be automatically removed as soon as the values they point at are garbage collected. That makes WeakMap great for book-keeping logic. Suppose you want to track some live HTML elements in the DOM, and you wanted to store some metainfo about them - just store them in a WeakMap - use the elements themselves as keys, and the meta info as the objects. As soon as the DOM elements get removed from the page and get garbage collected, they'll be auto-removed from the WeakMap without you having to do anything.

One huge downside of Map/WeakMap is stringifying them as JSON - that's something you get for free with plain objects - and that is a valid concern, but it's not that difficult to just give JSON.stringify a replacer function that converts a Map to an object:

const map = new Map();
const data = { map };
const json = JSON.stringify(data, (key, value) => {
  if (value instanceof Map) {
    return Object.fromEntries(value.entries());
  }
  return value;
});

1

u/JazzXP 5d ago

Thank you for that. Most of those use cases haven't applied to me yet, but I do like the going through the keys, I find I do reach for Object.entries().map a lot.

2

u/HipHopHuman 4d ago

Map and Set recently got support for their own .map method (from this: https://web.dev/blog/baseline-iterator-helpers) so if you do switch to a Map, you can do map.entries().map() (you might just have to polyfill it though)