JavaScript Object Methods

6 minute read

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 for Object.assign only for setter-aware merges
  • ✅ Use Object.entries + Object.fromEntries to map/filter object values
  • ✅ Use Object.hasOwn(obj, key) (ES2022) for existence checks
  • ✅ Use Object.freeze on 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…in if you only want own keys
  • ❌ Don’t use defineProperty for 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.

← Previous: JS Objects Next: JS Date Objects →

Updated: