Map 和 WeakMap 如何用 getOrInsert() 簡化鍵值初始化
Stop Writing if (!map.has(key)). Map Has getOrInsert()
JavaScript 的 Map 和 WeakMap 現已支援 getOrInsert() 與 getOrInsertComputed(),可在鍵不存在時儲存預設值並返回。計算版只會在鍵不存在時執行 callback;一般版本的預設值則會在呼叫前求值。MDN 標示這些方法自 2026 年 2 月起屬於 Baseline 2026 Newly available,Node 24 不支援。
TL;DR: map.getOrInsert(key, []) returns the value for key, and adds [] first if the key is missing. getOrInsertComputed() does the same with a callback, so the default is only built when it is needed. Both landed on Map and WeakMap, and they are Baseline 2026 Newly available.
The three lines everyone has typed
You have a Map and you want to group things into it. So you write the ritual:
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(item);
Three lookups to add one item. Or the shorter version that sneaks in a falsy bug the day someone stores 0:
const list = groups.get(key) || groups.set(key, []).get(key);
It works. It also looks like a riddle.
Every codebase has a getOrCreate helper for this, usually in a file called utils.js, next to three other helpers nobody remembers writing.
What it is
The TC39 "Upsert" proposal reached Stage 4 at the January 2026 meeting, and it adds two methods to both Map and WeakMap:
-
getOrInsert(key, defaultValue)returns the existing value forkey. If the key is missing, it storesdefaultValueand returns that. -
getOrInsertComputed(key, callback)does the same, but callscallback(key)only when the key is missing, and stores what it returns.
That is the whole API. No options object, no flags.
A minimal example
Counting words, the classic:
const counts = new Map();
for (const word of ["css", "js", "css", "html", "css"]) {
counts.set(word, counts.getOrInsert(word, 0) + 1);
}
console.log([...counts]);
// [["css", 3], ["js", 1], ["html", 1]]
And the grouping ritual from above becomes one line:
const orders = [
{ id: 1, customer: "ana" },
{ id: 2, customer: "ben" },
{ id: 3, customer: "ana" },
];
const byCustomer = new Map();
for (const order of orders) {
byCustomer.getOrInsert(order.customer, []).push(order.id);
}
console.log([...byCustomer]);
// [["ana", [1, 3]], ["ben", [2]]]
Both outputs are from a run in Chrome 155.
When the default is expensive
getOrInsert() is a normal function call, so its arguments are evaluated before it runs. If the default is costly, you pay for it even when the key already exists:
let built = 0;
const make = () => {
built++;
return 42;
};
const cache = new Map([["a", 1]]);
cache.getOrInsert("a", make());
console.log(built); // 1, built for nothing
built = 0;
cache.getOrInsertComputed("a", make);
console.log(built); // 0, key exists, callback skipped
cache.getOrInsertComputed("b", make);
console.log(built); // 1, key missing, callback ran once
Rule of thumb: a literal like 0 or [] goes to getOrInsert(). Anything that does real work goes to getOrInsertComputed().
The callback also receives the key, which keeps lazy caches short:
const upper = new Map();
console.log(upper.getOrInsertComputed("hello", (key) => key.toUpperCase()));
// "HELLO"
A realistic example: per-element state in a WeakMap
The same methods exist on WeakMap, which is where they get quietly useful. Say you want to track state for DOM elements without leaking memory when they are removed:
const state = new WeakMap();
function stateFor(element) {
return state.getOrInsertComputed(element, () => ({ clicks: 0 }));
}
document.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (!button) return;
const s = stateFor(button);
s.clicks++;
button.dataset.clicks = s.clicks;
});
The first click creates the state object, every later click reuses it, and when the button leaves the DOM the entry can be garbage collected. In a Chrome test page, clicking button A three times and a <span> inside button B once left data-clicks="3" and data-clicks="1", with the callback running exactly twice.
Can I use it?
From MDN's compatibility data:
| Runtime | Version |
|---|---|
| Chrome / Edge | 145 |
| Firefox | 144 |
| Safari | 26.2 |
| Node.js | 26.0.0 |
MDN marks it Baseline 2026, Newly available since February 2026. That means current browsers are fine, but anything older needs a fallback or a polyfill, since the methods simply do not exist there.
Gotchas
Node 24 does not have it. On Node 24.11 (V8 13.6),
typeof Map.prototype.getOrInsertis"undefined". If your code also runs on the server, check your Node version before you delete that helper.An existing
undefinedcounts as existing.new Map([["x", undefined]]).getOrInsert("x", 5)returnsundefinedand leaves it there. The check is "does the key exist", not "is the value truthy", which is exactly what the||trick gets wrong.The callback's return value wins. If the callback itself calls
map.set(key, ...), the method still stores what the callback returns. In a run, the map ended up holding"returned", not the value set inside the callback, with a size of 1. Better still, do not mutate the map from inside the callback.WeakMap keys still must be objects.
new WeakMap().getOrInsert("str", 1)throwsTypeError: Invalid value used as weak map key. The new methods do not relax the old rules.The error message blames the wrong thing. Pass a non-function to
getOrInsertComputed()and Chrome 155 throwsTypeError: Map.prototype.getOrInsertComputed is not a function. The method exists. It is your second argument that is not a function.
Wrap-up
The has, set, get dance was always three lookups wearing a trench coat. Find your getOrCreate helper, swap it for getOrInsert() or getOrInsertComputed(), and delete one more file from utils/.
Sources
- MDN: Map.prototype.getOrInsert()
- MDN: Map.prototype.getOrInsertComputed()
- MDN: WeakMap.prototype.getOrInsertComputed()
- MDN browser-compat-data: Map
- TC39: Upsert proposal
- TC39: Finished proposals
- ECMAScript spec: Map.prototype.getOrInsert
This article was written with the help of AI and checked against the sources above.
來源:DEV Community · dev.to