JavaScript Object Methods
The Object global hosts the static utilities you’ll lean on every day — converting between objects and arrays, merging, freezing, defining hidden properties, and walking own keys. This chapter is the practical reference: when to use each method, the differences between similar pairs, and the gotchas around references and shallow operations.
The Cheatsheet
| Method | Returns | Purpose |
|---|---|---|
Object.keys(obj) |
array of own enumerable string keys | iteration |
Object.values(obj) |
array of values | iteration |
Object.entries(obj) |
array of [key, value] pairs |
iteration |
Object.fromEntries(pairs) |
object built from pairs | inverse of entries |
Object.assign(target, ...sources) |
target (mutated) | shallow merge |
Object.freeze(obj) |
obj | shallow immutable |
Object.isFrozen(obj) |
boolean | check |
Object.seal(obj) |
obj | no add/remove, can modify existing |
Object.isSealed(obj) |
boolean | check |
Object.hasOwn(obj, key) |
boolean | own-property test (ES2022) |
Object.create(proto, descriptors?) |
new object | prototype-based creation |
Object.getPrototypeOf(obj) |
proto or null | prototype reflection |
Object.setPrototypeOf(obj, proto) |
obj | prototype mutation (slow!) |
Object.defineProperty(obj, key, desc) |
obj | low-level property control |
Object.getOwnPropertyDescriptor(obj, key) |
descriptor object | reflection |
Object.getOwnPropertyNames(obj) |
all own string keys (incl. non-enumerable) | reflection |
Object.groupBy(items, fn) |
grouped object (ES2024) | grouping |
Object.keys / values / entries
All three return own enumerable string keys — never inherited, never Symbols.
const user = { name: "Ada", age: 37, role: "admin" };
Object.keys(user); // ["name", "age", "role"]
Object.values(user); // ["Ada", 37, "admin"]
Object.entries(user); // [["name", "Ada"], ["age", 37], ["role", "admin"]]
// Iterate
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
{ name: "Ada", age: 37 }
│
▼ Object.entries
[["name", "Ada"], ["age", 37]]
│
▼ Object.fromEntries
{ name: "Ada", age: 37 }
Object.fromEntries — The Inverse
Object.fromEntries([["a", 1], ["b", 2]]);
// { a: 1, b: 2 }
// From a Map
const m = new Map([["x", 10], ["y", 20]]);
Object.fromEntries(m); // { x: 10, y: 20 }
// From URL search params
const params = new URLSearchParams("name=Ada&role=admin");
Object.fromEntries(params);
// { name: "Ada", role: "admin" }
The entries → transform → fromEntries round-trip is the standard “map over an object” pattern:
const prices = { apple: 1, banana: 0.5, cherry: 2 };
const taxed = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 1.1])
);
// { apple: 1.1, banana: 0.55, cherry: 2.2 }
const cheap = Object.fromEntries(
Object.entries(prices).filter(([, v]) => v < 1.5)
);
// { apple: 1, banana: 0.5 }
Object.assign — Shallow Merge
Object.assign(target, ...sources) copies own enumerable properties from each source onto target, mutating it, and returns the target.
const target = { a: 1, b: 2 };
const result = Object.assign(target, { b: 99, c: 3 }, { d: 4 });
result === target; // true ← same object, mutated
console.log(target); // { a: 1, b: 99, c: 3, d: 4 }
To merge without mutating, start from a fresh object:
const merged = Object.assign({}, defaults, overrides);
But the modern equivalent — and almost always the better choice — is the spread:
const merged = { ...defaults, ...overrides };
Object.assign |
Spread {...a, ...b} |
|
|---|---|---|
| Mutates? | ✅ first arg | ❌ |
| Triggers setters on target | ✅ yes | ❌ no |
| Readability | medium | high |
| Recommended | only when you need setters | ✅ default |
Warning: Both are shallow. Nested objects are shared by reference between source and merged result. Use structuredClone for deep copies.
Object.freeze and Object.seal
| freeze | seal | |
|---|---|---|
| Add new properties | ❌ | ❌ |
| Delete properties | ❌ | ❌ |
| Modify existing values | ❌ | ✅ |
| Reconfigure descriptors | ❌ | ❌ |
const config = Object.freeze({ apiKey: "abc", retries: 3 });
config.retries = 5; // silently ignored in non-strict; throws in strict mode
delete config.apiKey; // same
config.newField = 1; // same
Object.isFrozen(config); // true
freeze is shallow:
const obj = Object.freeze({ nested: { value: 1 } });
obj.nested.value = 99; // ✅ allowed — nested isn't frozen
obj.nested = {}; // ❌ blocked — top-level is frozen
A common deep-freeze helper:
function deepFreeze(obj) {
for (const value of Object.values(obj)) {
if (value && typeof value === "object") deepFreeze(value);
}
return Object.freeze(obj);
}
Tip: Freeze configuration objects exported from a module to prevent accidental mutation by consumers.
Object.hasOwn — Modern Existence Check
const obj = { name: "Ada" };
Object.hasOwn(obj, "name"); // true
Object.hasOwn(obj, "toString"); // false ← inherited, not own
"toString" in obj; // true ← walks prototype chain
obj.hasOwnProperty("name"); // true (legacy)
| Test | Walks prototype? | Works on null-proto objects? |
|---|---|---|
Object.hasOwn(obj, k) |
❌ | ✅ |
obj.hasOwnProperty(k) |
❌ | ❌ if Object.create(null) |
k in obj |
✅ | ✅ |
Prefer Object.hasOwn (ES2022) for new code.
Object.create and Prototypes
const animal = {
speak() { return `${this.name} speaks`; }
};
const dog = Object.create(animal);
dog.name = "Rex";
dog.speak(); // "Rex speaks"
Object.getPrototypeOf(dog) === animal; // true
// Pure dictionary — no Object.prototype methods at all
const dict = Object.create(null);
dict.toString; // undefined
"hasOwnProperty" in dict; // false ← clean slate
Object.create(null) is great for “real” hash maps — no risk of __proto__, constructor, or other inherited keys colliding with user data.
Object.defineProperty — Property Descriptors
Every property has a hidden descriptor with these flags:
| Flag | Default on obj.x = … |
Default on defineProperty |
|---|---|---|
value |
the value | undefined |
writable |
true |
false |
enumerable |
true |
false |
configurable |
true |
false |
const obj = {};
Object.defineProperty(obj, "version", {
value: "1.0.0",
writable: false,
enumerable: false, // hidden from Object.keys / for…in
configurable: false,
});
obj.version = "2.0"; // silently fails (throws in strict mode)
Object.keys(obj); // [] ← hidden!
Object.getOwnPropertyNames(obj); // ["version"] ← still listed here
Getters and setters are also defined this way:
const user = {
first: "Ada",
last: "Lovelace",
};
Object.defineProperty(user, "fullName", {
get() { return `${this.first} ${this.last}`; },
set(value) { [this.first, this.last] = value.split(" "); },
enumerable: true,
});
user.fullName; // "Ada Lovelace"
user.fullName = "Grace Hopper";
user.first; // "Grace"
Object.getOwnPropertyDescriptor(obj, key) lets you inspect a descriptor.
Object.groupBy (ES2024)
The native group-by you used to write with reduce:
const products = [
{ name: "apple", category: "fruit" },
{ name: "carrot", category: "veg" },
{ name: "banana", category: "fruit" },
];
Object.groupBy(products, p => p.category);
// {
// fruit: [{ name: "apple", ... }, { name: "banana", ... }],
// veg: [{ name: "carrot", ... }],
// }
There’s also Map.groupBy for non-string keys.
A Realistic Example — Object Pipeline
// API returned a flat list. We want a frozen lookup keyed by id, with
// computed `displayName`, and only active users.
const users = [
{ id: 1, first: "Ada", last: "L", active: true },
{ id: 2, first: "Lin", last: "T", active: false },
{ id: 3, first: "Rae", last: "K", active: true },
];
const byId = Object.freeze(
Object.fromEntries(
users
.filter(u => u.active)
.map(u => [u.id, {
...u,
displayName: `${u.first} ${u.last}`,
}])
)
);
console.log(byId[1].displayName); // "Ada L"
byId[1] = {}; // silently ignored (frozen)
filter → map → fromEntries → freeze is a powerful pipeline that combines half the methods in this chapter.
Common Mistakes
// ❌ Treating Object.assign as non-mutating
const merged = Object.assign(defaults, overrides);
// defaults is now mutated forever
// ✅
const merged = Object.assign({}, defaults, overrides);
// or
const merged = { ...defaults, ...overrides };
// ❌ Expecting freeze to be deep
const frozen = Object.freeze({ inner: { x: 1 } });
frozen.inner.x = 99; // happily succeeds
// ✅ Use deepFreeze or structuredClone first
// ❌ Object.keys on Maps / Sets
Object.keys(new Map([["a", 1]])); // [] ← Maps store entries differently
// ✅ Iterate the map directly: for (const [k, v] of myMap) { … }
Best Practices Checklist
- ✅ Use spread
{...a, ...b}for merging in new code; reach forObject.assignonly for setter-aware merges - ✅ Use
Object.entries+Object.fromEntriesto map/filter object values - ✅ Use
Object.hasOwn(obj, key)(ES2022) for existence checks - ✅ Use
Object.freezeon configuration / shared-state to prevent surprises - ✅ Reach for
Object.create(null)when you need a clean dictionary - ❌ Don’t assume freeze/seal/spread/assign are deep — they’re not
- ❌ Don’t iterate objects with
for…inif you only want own keys - ❌ Don’t use
definePropertyfor ordinary properties — overkill
What’s Next?
Strings, numbers, arrays, and objects are now in your toolbox. Time for dates and time — JavaScript’s date object has quirks worth knowing before you use it in production.