Objects
All three return own, enumerable properties only — inherited properties are excluded, unlike for...in.
🎯 What you'll learn
- Creating Objects
- Property Access
- Shorthand Syntax (ES6+)
- Object.keys / values / entries
- Copying and Merging
1Creating 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 |
2Property Access
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 key3Shorthand Syntax (ES6+)
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
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
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 syntaxBoth Object.assign and spread perform a shallow copy — nested objects are still shared by reference.
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 referencedFor 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:
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 enumeration7Freezing, 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.
8Optional Chaining and Nullish Coalescing
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, "", falseUnder 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."
const p = { x: 1, y: 2 }; // x and y likely both fit as in-object slotsOverflow 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.
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).
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 readThis 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
- Confusing
??with||—||treats any falsy value (0,"",false,NaN) as "missing";??only treatsnull/undefinedas missing. - Assuming spread/
Object.assigndeep-copies — both are shallow; mutating a nested object after copying affects the original too. delete obj.keyperformance — deleting properties can de-optimize object shape in some engines; prefer setting toundefinedor restructuring if done in hot code paths.for...inpicking up inherited properties — useObject.keys/Object.entriesunless you specifically want the whole chain.
📋 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🎤 Interview Q&A
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.
- 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.
- 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.
- 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.
- 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).
- 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.