# Objects

## 1. Creating Objects

| Syntax | Example | Notes |
|---|---|---|
| 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 / class | `new Dog()` | prototype = `Dog.prototype` |

## 2. Property 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
```

## 3. Shorthand 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
};
```

## 4. Object.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`.

## 5. Copying 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.).

## 6. Property 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
```

## 7. Freezing, Sealing, Preventing Extensions

| Method | Add new props | Delete props | Modify 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.

## 8. Optional 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
```

## 9. Under the Hood: In-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](diagrams/01-in-object-vs-overflow.svg)

### 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.

## 10. 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.

## 11. Quick Reference

```
{ ...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
```
