Engineering Notes
Home/Languages/JavaScript/Objects
Languages · JavaScript

Objects

All three return own, enumerable properties only — inherited properties are excluded, unlike for...in.

💻 Languages● Beginner⏱ 6 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Creating Objects
  • Property Access
  • Shorthand Syntax (ES6+)
  • Object.keys / values / entries
  • Copying and Merging

1Creating Objects

SyntaxExampleNotes
Object literal{ a: 1, b: 2 }most common, prototype = Object.prototype
new Object()new Object()rarely used, same result as {}
Object.create(proto)Object.create(base)explicit prototype control
Constructor / classnew Dog()prototype = Dog.prototype

2Property Access

js
const user = { "first-name": "Ana", age: 30 };

user.age;              // 30 — dot notation, key must be a valid identifier
user["first-name"];    // "Ana" — bracket notation, required for non-identifier keys
const key = "age";
user[key];             // 30 — bracket notation allows a dynamic/computed key

3Shorthand Syntax (ES6+)

js
const name = "Ana", age = 30;

const user = { name, age };              // property shorthand: { name: name, age: age }
const obj = {
  greet() { return "hi"; },              // method shorthand: greet: function() {...}
  [`computed_${name}`]: true,             // computed property key
};

4Object.keys / values / entries

js
const point = { x: 1, y: 2 };

Object.keys(point);    // ["x", "y"]
Object.values(point);  // [1, 2]
Object.entries(point); // [["x", 1], ["y", 2]]

for (const [key, value] of Object.entries(point)) {
  console.log(key, value);
}

All three return own, enumerable properties only — inherited properties are excluded, unlike for...in.

5Copying and Merging

js
const defaults = { theme: "light", retries: 3 };
const overrides = { retries: 5 };

const merged = { ...defaults, ...overrides };     // spread: { theme: "light", retries: 5 }
const merged2 = Object.assign({}, defaults, overrides); // equivalent, older syntax

Both Object.assign and spread perform a shallow copy — nested objects are still shared by reference.

js
const original = { nested: { count: 1 } };
const shallow = { ...original };
shallow.nested.count = 99;
console.log(original.nested.count); // 99 — the nested object was NOT copied, only referenced

For a true deep copy: structuredClone(original) (modern runtimes) or JSON.parse(JSON.stringify(original)) (loses functions, undefined, Date objects, etc.).

6Property Descriptors

Every property has hidden metadata beyond its value, controlling how it behaves:

js
const obj = {};
Object.defineProperty(obj, "id", {
  value: 42,
  writable: false,   // can't be reassigned
  enumerable: false,  // won't show up in Object.keys/for-in/JSON.stringify
  configurable: false, // can't be deleted or redefined
});

obj.id = 999;         // silently fails (or throws in strict mode)
console.log(obj.id);  // 42
console.log(Object.keys(obj)); // [] — id is hidden from enumeration

7Freezing, Sealing, Preventing Extensions

MethodAdd new propsDelete propsModify existing values
Object.preventExtensions(obj)❌✅✅
Object.seal(obj)❌❌✅
Object.freeze(obj)❌❌❌

Object.freeze is shallow — nested objects inside a frozen object remain fully mutable unless frozen separately.

8Optional Chaining and Nullish Coalescing

js
const user = { address: { city: "Berlin" } };

user.address?.city;          // "Berlin"
user.contact?.phone;         // undefined — no error, short-circuits instead of throwing
user.contact?.phone ?? "N/A"; // "N/A" — nullish coalescing: fallback only for null/undefined
user.retries ?? 3;            // uses user.retries if it's not null/undefined, even if it's 0
user.retries || 3;            // WRONG for this purpose — || also replaces 0, "", false
Under the HoodIn-Object Slots, Overflow Storage, and Dictionary Mode▾

An object literal isn't a generic hash map by default — treating it as one internally would waste both memory and speed for the overwhelmingly common case of objects with a small, predictable set of properties. Engines use a tiered storage strategy instead.

In-object properties: fixed slots on the object itself

When an object is created, the engine reserves a small, fixed number of property slots directly inside the object's own memory, sized according to its hidden class (see the Prototypes topic). Reading obj.x when x is an in-object property is a single fixed-offset memory read — no separate allocation, no lookup table, just "offset N from the start of this object."

js
const p = { x: 1, y: 2 }; // x and y likely both fit as in-object slots

Overflow into a separate properties backing store

Once an object accumulates more properties than its in-object slot budget allows, additional properties spill into a separate, dynamically-growable properties array, referenced by a pointer from the main object. This is conceptually the same growth/resize trade-off as the Arrays topic's push-amortization — the backing array grows (and gets copied) as needed — but it means property number N+1 costs one extra pointer dereference compared to property 1..N.

The first few properties sit in fast in-object slots; extra properties spill into a separate overflow array
The first few properties sit in fast in-object slots; extra properties spill into a separate overflow array

Dictionary mode: when an object gives up on hidden classes entirely

If an object is used in ways the hidden-class system can't efficiently track — properties added and deleted repeatedly and unpredictably, an enormous number of distinct property names, or properties added via Object.defineProperty with unusual descriptors — the engine gives up on the fast in-object-slot system entirely and demotes the object to dictionary mode: its properties are now stored in a real per-object hash table (the same fundamental structure as the Hashing topic's hash tables, and conceptually similar to how a Map stores its entries).

js
const obj = {};
for (let i = 0; i < 2000; i++) {
  obj[`dynamicKey${i}`] = i;      // huge number of unpredictable keys
  if (i % 3 === 0) delete obj[`dynamicKey${i - 1}`]; // frequent deletes
}
// this object has very likely been demoted to dictionary mode —
// property access on it now costs a real hash lookup, not a fixed-offset read

This is the deeper reason section 9's "delete can de-optimize object shape" pitfall exists: deletion doesn't just remove a value, it can push the entire object out of the fast hidden-class system into a permanently slower storage mode — an object generally cannot transition back to fast mode once demoted.

Why plain objects and Map diverge in practice

A plain object used as intended (fixed, known keys, set once) stays on the fast in-object/hidden-class path the whole time. A plain object used as a general-purpose dictionary with unpredictable keys tends to end up in dictionary mode anyway — which is precisely the scenario the Map topic recommends Map for: Map's ordered-hash-table design is built for exactly this access pattern from the start, rather than falling back into it as a demotion.

⚠️ Common Pitfalls

  1. Confusing ?? with || — || treats any falsy value (0, "", false, NaN) as "missing"; ?? only treats null/undefined as missing.
  2. Assuming spread/Object.assign deep-copies — both are shallow; mutating a nested object after copying affects the original too.
  3. delete obj.key performance — deleting properties can de-optimize object shape in some engines; prefer setting to undefined or restructuring if done in hot code paths.
  4. for...in picking up inherited properties — use Object.keys/Object.entries unless you specifically want the whole chain.

📋 Quick Reference

text
{ ...spread }              → shallow copy / merge
Object.keys/values/entries → own enumerable properties only
obj?.prop                  → safe access, no throw on null/undefined
a ?? b                     → b only if a is null or undefined
a || b                     → b if a is ANY falsy value

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How do you copy objects in JavaScript?core▾

Shallow copies: spread {...obj} or Object.assign. Deep copies: structuredClone(obj) (handles dates, maps, cycles; not functions or class prototypes). JSON.parse(JSON.stringify()) loses dates, undefined, functions and fails on cycles.

Follow-ups interviewers ask
  • Why does shallow copy matter? Nested objects are still shared.
Q2How do you make objects immutable?core▾

Object.freeze prevents adding/changing/removing properties (shallow); deep freezing requires recursion. In practice, immutable update patterns (spread) or libraries like Immer are used.

Follow-ups interviewers ask
  • Object.seal vs freeze? seal allows changing existing values, not adding/removing.
Q3How do you iterate over objects?core▾

Object.keys/values/entries for own enumerable string keys, for…in includes inherited enumerable keys, Reflect.ownKeys includes symbols and non-enumerables.

Follow-ups interviewers ask
  • Is key order guaranteed? Integer-like keys ascending, then string keys in insertion order, then symbols.
Q4What are property descriptors?senior▾

Each property has attributes: value, writable, enumerable, configurable (or get/set for accessors), managed with Object.defineProperty and inspected with Object.getOwnPropertyDescriptor.

Follow-ups interviewers ask
  • Use case? Non-enumerable or read-only properties in libraries.
Q5When should you use a Map instead of an object?core▾

When keys aren't strings/symbols, keys are added/removed frequently, you need insertion order and size, or keys come from user input (avoids prototype pollution).

Follow-ups interviewers ask
  • Object advantage? JSON serialization and literal syntax.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.