Arrays have a rich set of iteration methods; plain objects do not. You cannot call .map() or .filter() on a {}. The trick I use constantly is to bridge the two worlds: turn the object into an array of [key, value] pairs with Object.entries, do array work on it, then turn the result back into an object with Object.fromEntries. That round-trip is the single most useful object idiom in modern JavaScript, and it is what this page is really about. Along the way: replacing hasOwnProperty with Object.hasOwn, the fact that Object.freeze only freezes one level deep, and when a plain object stops being the right container and you should reach for a Map.
The three iterators: keys, values, entries
All three take an object and hand you back an array, so you can iterate with for...of, .forEach, or any array method:
const prices = { coffee: 3, tea: 2, juice: 4 };
Object.keys(prices); // ["coffee", "tea", "juice"]
Object.values(prices); // [3, 2, 4]
Object.entries(prices); // [["coffee", 3], ["tea", 2], ["juice", 4]]Object.entries is the one I use most, because each pair destructures cleanly in a loop:
for (const [name, price] of Object.entries(prices)) {
console.log(`${name}: $${price}`);
}These only see an object's own, enumerable, string-keyed properties, which is almost always what you want. That is the key difference from the old for...in loop, which also walks inherited properties up the prototype chain and is why for...in code is littered with hasOwnProperty guards. With Object.keys and friends, you do not need the guard at all.
The round-trip: entries to fromEntries
Object.fromEntries is the inverse of Object.entries: give it an array of [key, value] pairs and it builds an object. Put the two together and you can map and filter over an object as if it were an array, then come back to object shape.
Filter an object down to the entries you want:
const inventory = { apples: 5, bananas: 0, cherries: 12, dates: 0 };
// Keep only the items still in stock
const inStock = Object.fromEntries(
Object.entries(inventory).filter(([, qty]) => qty > 0)
);
// { apples: 5, cherries: 12 }Transform the values:
// Apply a 10% markup to every price
const marked = Object.fromEntries(
Object.entries(prices).map(([name, price]) => [name, price * 1.1])
);Once you internalize this pattern, a lot of awkward object code disappears. A small helper makes it read even better, and the te prefix keeps it from colliding with anything:
// Map over an object's values, keep its keys
function teMapValues(obj, fn) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, fn(value, key)])
);
}
teMapValues(prices, (p) => p * 1.1);Object.fromEntries also takes anything that yields pairs, including a Map, so it is the clean way back from a Map to a plain object: Object.fromEntries(myMap). The reverse, new Map(Object.entries(obj)), gets you from object to Map. If you are building an object out of two parallel arrays or parsing structured input, the entries form is usually the most direct route. (Object.fromEntries landed in ES2019 and is in every current runtime.)
A close cousin of this work is reading values out of an object into variables, which is what JavaScript destructuring is for, and the array side of these iterators lines up with the JavaScript array methods you would chain after Object.entries.
Object.hasOwn: the modern hasOwnProperty
When you do need to ask "does this object have this key as its own property," the old answer was obj.hasOwnProperty(key). The problem: hasOwnProperty lives on the prototype, so it breaks the moment the object does not inherit from Object.prototype (more on that below), or when the object itself has a property literally named hasOwnProperty. The defensive workaround people wrote for years was ugly:
// The old guard you no longer need
Object.prototype.hasOwnProperty.call(obj, key);Object.hasOwn(obj, key) replaces all of that. It is a static method, so it never depends on the object's prototype:
const settings = { darkMode: true };
Object.hasOwn(settings, "darkMode"); // true
Object.hasOwn(settings, "toString"); // false (inherited, not own)Object.hasOwn reached Baseline in 2022 (it is part of ES2022) and shipped in Node.js 16.9.0, so on any current Node or browser it is the default I reach for. The null-prototype objects in the next section are exactly the case that makes the old obj.hasOwnProperty(key) blow up, and the precise reason Object.hasOwn exists.
Object.freeze is shallow (this trips everyone up)
Object.freeze(obj) makes an object's own properties non-writable and non-configurable: you cannot reassign them, add new ones, or delete existing ones. It is how I lock down a constants object. But the gotcha that bites people is that the freeze is shallow. It applies only to the immediate properties. Any nested object stays fully mutable:
const config = Object.freeze({
name: "prod",
limits: { maxUsers: 100 },
});
config.name = "dev"; // silently ignored (throws in strict mode)
config.limits.maxUsers = 5; // works. limits is a different object, not frozenThat second line is the trap. config is frozen, but config.limits points at a separate object that freeze never touched. If you want true immutability all the way down, you have to recurse. Here is the deep-freeze helper I keep around:
function teDeepFreeze(obj) {
// Freeze the object first, then recurse. A cyclic reference (a child that
// points back at an ancestor) then lands on an already-frozen object and
// the isFrozen check stops it, instead of looping forever.
Object.freeze(obj);
for (const value of Object.values(obj)) {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
teDeepFreeze(value);
}
}
return obj;
}The order matters: freeze first, recurse second. If you recurse before freezing, a cyclic reference never sees a frozen object and the Object.isFrozen(value) guard cannot fire, so the function loops forever. Freezing the current object up front means a cycle reaches an already-frozen node and the guard short-circuits it. Object.isFrozen(obj) is also how you ask after the fact whether an object is locked.
There is a lighter-weight sibling, Object.seal, which prevents adding or deleting properties but still lets you change the values of existing ones. It is the right tool when you want a fixed shape but mutable contents. Object.freeze is Object.seal plus making the existing values read-only too. Both are shallow, so the same nested-object caveat applies to Object.seal.
If your goal is producing a new merged object rather than locking one down, the patterns in merging objects in JavaScript cover Object.assign and spread, including the same shallow-vs-deep distinction in a copy context.
Object.create(null) versus a Map
A plain {} inherits from Object.prototype, which means it carries inherited keys like toString, constructor, and hasOwnProperty. When you use an object as a lookup table built from external data, that inheritance is a liability: a key named "constructor" collides with something real, and "hasOwnProperty" in the data shadows the method you might call. Object.create(null) makes a "null-prototype" object with no inherited anything, a clean dictionary:
const dict = Object.create(null);
dict.constructor = "totally fine now";
// But it has no inherited methods at all:
dict.hasOwnProperty; // undefined
Object.hasOwn(dict, "foo"); // use this instead, it is staticThis is a real-world footgun. I have been bitten by a library (an older query-string parser) returning Object.create(null): my code called .hasOwnProperty() on the result and threw, because the method was not there. Object.hasOwn sidesteps it entirely, which is the connection to the section above. Parsing URL parameters is exactly that kind of data, see parsing query string parameters for the dictionary-shaped result that benefits from this.
So when do you use a null-prototype object versus a real Map? My rule:
- Null-prototype object when keys are strings, the shape is roughly static, and you want a plain JSON-serializable object at the end (a config blob, a parsed-params bag). It serializes with
JSON.stringifyfor free; aMapdoes not. Mapwhen keys can be non-strings (objects, numbers that must stay numbers), or when you add and delete entries frequently and want.has,.get,.set,.delete, and a reliable.size.Mappreserves insertion order and is built for churn.
For a string-keyed thing you read more than you write, the object is simpler. For arbitrary keys or heavy mutation, reach for Map.
See also
- JavaScript array methods: the
map,filter, andreduceyou chain afterObject.entries. - Merging objects in JavaScript:
Object.assignand spread, and the shallow-versus-deep copy distinction. - JavaScript destructuring: pull keys and values straight out of objects (and the pairs
Object.entrieshands you). - Parsing query string parameters: a real dictionary-shaped result that pairs well with null-prototype objects.
Sources
Authoritative references this article was fact-checked against.
- Object.entries() (MDN)developer.mozilla.org
- Object.fromEntries() (MDN)developer.mozilla.org
- Object.hasOwn() (MDN)developer.mozilla.org
- Object.freeze() (MDN)developer.mozilla.org
- Object.create() (MDN)developer.mozilla.org





