JS Objects - Silent Mutation Broke User Sessions
Object mutation from reference copy in JavaScript corrupted user sessions: changing one user's email altered others.
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- JavaScript objects store key-value pairs, bundling related data into one unit
- Two access methods: dot notation (static keys) and bracket notation (dynamic keys)
- Objects are reference types: assignment copies the reference, not the value
- Accessing a missing property returns undefined silently — no error thrown
- Performance tip: object property lookup is O(1) on average, but deleting properties can deoptimize hidden classes
- Production insight: a typo in property name causes undefined and silent failures; use optional chaining or strict checks to catch early
Think of a JavaScript object like a contact card in your phone. That card has a name, a phone number, an email, and maybe a photo — all bundled together under one person. In JavaScript, an object lets you group related pieces of information about one 'thing' into a single place, instead of scattering them across a dozen separate variables. Just like you'd look up someone by their name to find their number, you look up a property by its key to find its value.
Every real-world app you use — Instagram, Spotify, your bank's website — is built on data. A song has a title, an artist, a duration, and a genre. A user has a name, an email, and an age. If you tried to store all of that in separate, disconnected variables, your code would become impossible to manage before you even got started. JavaScript objects are the solution to this exact problem, and they're at the heart of almost everything you'll ever build on the web.
Before objects, imagine needing to track five pieces of information about 100 users. That's 500 variables. And when you pass data to a function, you'd need to pass all five separately every single time. Objects let you wrap all related data into one tidy package and hand it around your code as a single unit. They reflect how we naturally think about things in the real world — as entities with multiple characteristics.
By the end of this article you'll know how to create objects from scratch, read and update their properties using both dot notation and bracket notation, add and delete properties on the fly, loop over an object's contents, and understand the difference between primitive values and objects (a concept that trips up almost every beginner). You'll be ready to use objects confidently in real projects.
What Is a JavaScript Object and How Do You Create One?
An object in JavaScript is a collection of key-value pairs. The 'key' is the name of a piece of information (like 'firstName'), and the 'value' is the actual data stored under that name (like 'Sarah'). Together, each key-value pair is called a property.
The most common and readable way to create an object is with curly braces {}. This is called an object literal, and it's the go-to syntax for most JavaScript developers.
Inside the curly braces, you list your properties separated by commas. Each property is written as key: value. The key is usually written without quotes (unless it contains spaces or special characters), and the value can be any data type — a string, a number, a boolean, an array, or even another object.
Notice how grouping related data this way instantly makes your code more readable. Instead of six separate variables floating around, everything about a user lives in one place. That's the core win objects give you.
const when declaring objects unless you need to reassign the entire variable to a new object. const doesn't freeze the object — you can still change its properties freely. It just prevents you from accidentally pointing the variable at a completely different object later.Object() – it's slower and adds no benefit.Reading and Updating Object Properties — Dot vs Bracket Notation
Once you have an object, you need to get data out of it and put new data in. There are two ways to access a property: dot notation and bracket notation. Both do the same job — pick the right one for the situation.
Dot notation (object.propertyName) is cleaner and the one you'll use 90% of the time. Bracket notation (object['propertyName']) is more flexible — it lets you use a variable as the key, which is powerful when you don't know the property name until the code actually runs.
Updating a property is just as simple — you access it the same way and assign a new value with =. If the property doesn't exist yet, this same syntax creates it. There's no separate 'add property' command. Assignment does it all.
Deleting a property uses the delete keyword. It completely removes the key-value pair from the object, not just sets it to null. Use this with care — deleting properties can sometimes affect performance in JavaScript engines that optimise objects.
undefined. This is a common source of bugs. If you see undefined where you expected a real value, check your property name for typos first. Keys are case-sensitive, so bookListing.Title and bookListing.title are completely different.Looping Over an Object and Nesting Objects Inside Objects
Most real-world objects contain more properties than you'd want to type out one by one. JavaScript gives you a for...in loop specifically designed to iterate over every key in an object. On each iteration, the loop gives you the current key as a string, and you use bracket notation to get the value.
Objects can also be nested — meaning a property's value can itself be an object. This is how real data is structured. Think of a user profile that has an address: the address has a street, a city, and a postcode. It makes sense to group those three inside their own nested object rather than flattening everything into one level.
To access deeply nested properties, you just chain dot notation: user.address.city. Read it left to right — start at user, go into the address object, then grab the city property inside it.
Be careful with for...in — it can loop over inherited properties from the object's prototype chain in some situations. Using hasOwnProperty() is a defensive habit that guarantees you only touch properties you actually defined.
${userAccount[key]}), JavaScript converts it to the string [object Object] — not the full contents. To see nested object contents properly, use console.log(key, userAccount[key]) without a template literal, or use JSON.stringify(userAccount[key]) to convert it to a readable JSON string.Object.keys() or hasOwnProperty to avoid prototype pollution.Object.entries() with for...of.Object.keys() returns only own property names.Objects Are Reference Types — the Concept That Trips Everyone Up
Here's the thing that bites almost every beginner at some point: objects in JavaScript don't behave like numbers or strings when you copy or compare them.
With a number or string (primitive types), when you assign one variable to another, you get a true independent copy. Change one and the other is unaffected.
With objects, when you assign one variable to another, you don't copy the object — you copy the reference to it. Both variables now point at the exact same object in memory. So if you change a property through one variable, the other variable reflects that change too, because they're both looking at the same thing.
This also means two objects that look identical are NOT equal when you compare them with ===, because === checks whether they're the same reference (the same object in memory), not whether they have the same content.
To make a true shallow copy of an object, use the spread operator ({...originalObject}) or Object.assign(). For deeply nested objects, you need a deep clone, which JSON.parse(JSON.stringify(obj)) handles for simple cases.
{...obj}) for shallow copies and JSON.parse(JSON.stringify(obj)) for deep copies will impress any interviewer.Object Spreading and Merging – Combining Objects Safely
When you need to combine multiple objects into one, or create a new object from an existing one with modifications, the spread operator (...) is your best friend. Introduced in ES6, spread allows you to copy properties from one object into another elegantly.
For merging two objects, you can do: const merged = { ...objA, ...objB }. If both have the same property, the later source overwrites the earlier one. This is a shallow merge — nested objects are still shared references.
Object.assign() is the older alternative and does the same shallow merge. The spread syntax is generally preferred for readability.
Be careful with deep merging. A simple spread will not clone nested objects; they remain references. For a deep merge, you need to recursively merge or use a utility like Lodash's or _.merge()structuredClone() combined with spread.
structuredClone() for deep cloning, or manually deep copy nested levels.Property Existence Checks: The Difference Between `in`, `hasOwnProperty`, and Optional Chaining
You'd think checking if a property exists is trivial. Then you ship a bug where undefined meant "property missing" but actually meant "value was legitimately undefined." Welcome to the real world.
Here's the deal: obj.prop === undefined fails when the property exists but holds undefined deliberately. 'prop' in obj checks the entire prototype chain, which can inherit properties from Object.prototype like toString. That's almost never what you want.
Object.hasOwn(obj, 'prop') is the modern fix. It checks own properties only, no prototype noise. It's faster than hasOwnProperty.call() and works even if someone overrides obj.hasOwnProperty (yes, I've seen it in legacy code).
Optional chaining (obj?.deep?.nested) doesn't check existence — it short-circuits on null or undefined. Use it for safe access, not existence checks. Mix the two and you'll suppress real bugs.
Object.hasOwn() for own-property checks.Object.hasOwn() for own-property existence. Use optional chaining only for safe access, never for existence logic.Deleting Properties: The `delete` Operator Isn't Free — And What It Can't Touch
Deleting a property sounds simple: delete obj.prop. But this operator changes the object's hidden class under the hood, forcing V8 to deopt and rebuild the object shape. Do this in a hot loop and watch your frame budget evaporate.
Worse: delete can't remove non-configurable properties. Try deleting Math.PI and you'll get a silent false. In strict mode, it throws. If you're deleting array elements with delete, you leave a hole — arr.length doesn't shrink. Use or splice() instead.filter()
The practical fix: if performance matters, set the property to undefined and add an existence guard. This preserves object shape. For dynamic cleanup, consider Map — Map.delete() is O(1) with no hidden-class penalties.
Only use delete when you genuinely need to drop the key from enumeration or serialization. Otherwise, undefined is your faster, safer alternative.
delete in performance-sensitive code. Set properties to undefined unless you need the key gone from JSON serialization or enumeration.Object.keys vs Object.values vs Object.entries — Pick the Right Weapon for the Job
New devs treat these three as interchangeable. They're not. Each one solves a different problem. Use Object.keys() when you need to do something with the keys themselves — like checking for a property, or hashing by key name. Use Object.values() when you only care about the data payload, like summing numbers or filtering a list. Use Object.entries() when you need both, like building a new object from a transform.
Performance? In modern V8, Object.keys() is micro-optimized. It's about 2x faster than Object.entries() for small objects because it avoids allocating key-value pairs. But don't micro-optimize prematurely — pick for readability first.
The real trap: for...in loops over enumerable properties, including inherited ones. The Object.* static methods only return own enumerable properties. Use them. Always. Unless you have a specific reason to walk the prototype chain (you probably don't).
Pro tip: destructure in for...of with Object.entries() for clean key-value iteration without the for...in headache.
OOP Pillars Through JavaScript Objects — Abstraction, Inheritance, Polymorphism, Encapsulation
Most devs think OOP is only for classes. That's marketing fluff. JavaScript objects are the original OO citizens, and they nail all four pillars without a single class keyword. Why? Because you need code that hides complexity (abstraction), owns its data (encapsulation), reuses structure (inheritance), and bends behavior at runtime (polymorphism). Without these, your objects become leaky bags of state.
Encapsulation happens by keeping data behind closure walls or private # fields. Abstraction means exposing a clean method like .pay() while the tax logic stays hidden. Inheritance works through prototypes — Object.create() links objects cheaply. Polymorphism? Objects override inherited methods on the fly. One method, ten different animals, zero if-else chains.speak()
Stop treating objects as dumb containers. Apply the pillars and watch your code become self-documenting, testable, and resistant to spaghetti refactors.
# private fields (ES2022+) — they're prototype-native and memory efficient.The Silent Mutation That Corrupted User Sessions
JSON.stringify()).- Always assume object assignment is a reference copy unless you explicitly copy the object.
- Use spread operator or structuredClone() when you need an independent snapshot.
- Treat object mutation as a shared state risk; prefer immutable patterns in production.
JSON.stringify())._.isEqual().console.log('Original:', originalObj, 'Copy:', copyObj);console.log('Same reference?', originalObj === copyObj);Key takeaways
{ ...originalObject } (spread) to get a real shallow copy.undefined silentlyCommon mistakes to avoid
3 patternsTreating object assignment as a copy
const newObj = { ...existingObj } to create a proper shallow copy.Accessing a property that doesn't exist and assuming the code will error
undefined silently for missing properties, causing downstream bugs that are hard to trace.obj?.propertyName) for uncertain access, or check 'propertyName' in obj before accessing. Never assume a property exists unless you created it.Using `===` to compare two objects with identical content
{ name: 'Alex' } === { name: 'Alex' } evaluates to false because JavaScript compares references, not content.JSON.stringify(objA) === JSON.stringify(objB) for simple flat objects, or a library like Lodash's _.isEqual() for nested ones.Interview Questions on This Topic
What is the difference between dot notation and bracket notation in JavaScript, and when would you specifically choose bracket notation over dot notation?
obj.key) is cleaner and faster for property access when you know the exact key name at development time. Bracket notation (obj['key']) is necessary when the key name contains spaces or special characters, is a number, or is stored in a variable (dynamic key lookup). Bracket notation evaluates an expression inside the brackets, so it can also be used for computed properties. In production, prefer dot notation for readability and minor performance gains.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's JS Basics. Mark it forged?
7 min read · try the examples if you haven't