Object Literals, Property Attributes & Object.defineProperty
Dive beneath object literals. Explore property descriptors: writable, enumerable, configurable, getters, setters, and object freezing.
By the end, you’ll be able to…
- Inspect and configure property descriptors using Object.getOwnPropertyDescriptor and Object.defineProperty.
- Contrast data descriptors with accessor descriptors (getters/setters).
- Enforce immutability boundaries using Object.preventExtensions, Object.seal, and Object.freeze.
01Beyond Simple Key-Value Pairs
In everyday JavaScript, we create objects using simple object literal syntax: const user = { name: 'Ada' }. While this appears to be a basic dictionary, ECMAScript objects are vastly more sophisticated. Every property on an object is governed by a Property Descriptor: a record of internal boolean flags that control how the JavaScript engine treats that property.
There are two kinds of property descriptors: Data Descriptors and Accessor Descriptors. Data descriptors possess a value and a writable flag. Accessor descriptors define get and set functions. Both share enumerable (whether the key appears in for...in loops and Object.keys()) and configurable (whether the descriptor can be modified or deleted).
Every property on a JavaScript object is governed by an underlying set of attributes called Property Descriptors. When you create a property using standard assignment (obj.x = 10), JavaScript creates a Data Descriptor where writable, enumerable, and configurable all default to true.
However, when defining properties via Object.defineProperty, omitted descriptor flags default to false! A property created with Object.defineProperty(obj, 'id', { value: 101 }) will be read-only (writable: false), invisible to for...in loops and Object.keys (enumerable: false), and impossible to delete or reconfigure (configurable: false).
02Defining Read-Only and Hidden Properties
Using Object.defineProperty, you can configure properties with surgical precision. If you set writable: false, attempts to mutate the property in strict mode will throw a TypeError: Cannot assign to read only property.
If you set enumerable: false, the property becomes invisible to Object.keys(), Object.entries(), and for...in loops, while remaining directly accessible via direct property lookup. This is how native JavaScript engine APIs hide built-in methods like Array.prototype.toString from polluting your for...in loops.
03Object Integrity Levels: Freeze vs Seal
ECMAScript provides three built-in integrity levels to protect objects from modification: Object.preventExtensions(), Object.seal(), and Object.freeze().
Object.preventExtensions(obj) prevents new properties from being added, but existing properties can still be modified or deleted. Object.seal(obj) goes further: no new properties can be added and existing properties cannot be deleted (configurable: false), but existing writable properties can still be updated. Object.freeze(obj) is the ultimate lock: it seals the object and marks all data properties as writable: false, creating a shallowly immutable object.
04Why This Matters in Production
In production architecture, libraries and frameworks rely heavily on property descriptors to protect internal state, implement reactive getters and setters (Vue 2 and MobX), and enforce domain invariants in Domain-Driven Design (DDD).
Note that Object.freeze is shallow: nested objects inside a frozen parent can still be mutated unless you apply a recursive deepFreeze utility. Enforcing immutability prevents rogue third-party packages or junior code from accidentally overriding critical shared configurations.
The ideas worth keeping.
- Property descriptors control writable, enumerable, and configurable behaviors.
- Object.freeze creates shallowly immutable objects; deep immutability requires recursive freezing.
- Non-enumerable properties provide clean encapsulation without polluting iteration loops.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Deep Freeze Implementation
Write a function deepFreeze(obj) that recursively freezes an object and all its nested object properties.
Explore a worked solution
function deepFreeze(obj) { Object.freeze(obj); for (const value of Object.values(obj)) { if (value && typeof value === 'object' && !Object.isFrozen(value)) { deepFreeze(value); } } return obj;}Recursively traversing object values and freezing each nested heap object establishes true, deep immutability.
Exercise 2: Non-Enumerable Secret Key
Attach a non-enumerable property secretToken to an existing object authState so it does not appear in Object.keys() but can be read directly.
Explore a worked solution
function attachSecret(authState, token) { Object.defineProperty(authState, 'secretToken', { value: token, enumerable: false, writable: false, configurable: false }); return authState;}Setting enumerable: false hides the property from enumeration loops and serialization utilities like JSON.stringify.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Deep Freeze Implementation
Write a function deepFreeze(obj) that recursively freezes an object and all its nested object properties.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.