JavaScript Property Descriptors
Learn how JavaScript property flags (writable, enumerable, configurable) and descriptors work, including data vs accessor descriptors and defineProperty.
JavaScript's property flags and descriptors provide precise control over object properties, enabling robust and secure application development. This article explores these features in detail, providing practical insights and code examples to help you effectively manage property behavior.
Understanding JavaScript Property Attributes
JavaScript objects are collections of properties, and each property has associated attributes that define its behavior. These attributes, often referred to as property flags, include:
- Writable: Determines if the property's value can be changed.
- Enumerable: Controls if the property is visible during enumeration, such as in a
for...inloop. - Configurable: Specifies whether the property can be deleted or modified.
These flags are crucial for controlling access to object properties, ensuring data integrity, and implementing encapsulation in JavaScript applications.
Diving Into Property Descriptors
Property descriptors provide detailed information about an object's property, encapsulating its value and flags. They are retrieved using Object.getOwnPropertyDescriptor(obj, propName) and set using Object.defineProperty(obj, propName, descriptor). A property descriptor object may contain:
value: The value associated with the property.writable: Indicates if the property value can be changed.enumerable: Denotes whether the property is enumerable.configurable: Determines if the property descriptor can be changed and if the property can be deleted from the object.
Note: When you create a property the normal way (user.name = "John"), all three flags are set to true. But when defining a new property via Object.defineProperty, any unspecified flag defaults to false.
Object.getOwnPropertyDescriptor only looks at the object's own properties. If you ask for a property the object inherits from its prototype (or a property that doesn't exist at all), it returns undefined.
To learn more about how objects inherit properties, see Prototypal Inheritance.
Data Descriptors vs. Accessor Descriptors
So far we've described data descriptors, which store a value together with the writable flag. JavaScript also supports accessor descriptors, which replace value/writable with getter and setter functions:
get: a function called when the property is read (takes no arguments).set: a function called when the property is assigned to (receives the new value).
A descriptor is either a data descriptor or an accessor descriptor — never both. Combining value/writable with get/set throws an error. Both kinds still share the enumerable and configurable flags.
Accessor properties are how you compute a value on read or validate one on write. Here we expose a fullName accessor backed by two data properties:
For a fuller treatment of get/set syntax (including the shorthand inside object literals), see Property Getters and Setters. Because getters and setters run with this bound to the object, it also helps to understand Object Methods and "this".
Defining and Reading Many Properties at Once
For working with several properties in one step, JavaScript provides the plural counterparts of the methods above:
Object.defineProperties(obj, descriptors)defines multiple properties from a map of descriptors.Object.getOwnPropertyDescriptors(obj)returns descriptors for all own properties (including non-enumerable and symbol keys) as a single object.
Object.getOwnPropertyDescriptors is especially useful for cloning an object with its flags — a plain spread or Object.assign copies values but resets every flag to true and skips accessors.
Manipulating Property Flags
Understanding and manipulating property flags are crucial for effective JavaScript development. Let’s explore how to control these flags to fine-tune property behavior.
Making a Property Non-writable
Preventing modifications to a property ensures data consistency. This can be achieved by setting the writable flag to false.
How the failed assignment behaves depends on the mode the code runs in. In non-strict mode, writing to a non-writable property fails silently: the assignment is simply ignored, no error is thrown, and execution continues — which can hide bugs. In strict mode ("use strict", and the default inside ES modules and class bodies), the same assignment throws a TypeError. The rule applies to any operation that violates a flag: deleting a non-configurable property or adding a property to a non-extensible object also fails silently in non-strict mode and throws in strict mode.
Hiding a Property from Enumeration
Sometimes, it's necessary to hide properties from enumeration processes, such as for...in loops. This can be done by setting the enumerable flag to false.
Preventing Property Deletion and Modification
To ensure a property remains a constant part of an object, set the configurable flag to false.
Marking a property non-configurable is a one-way operation — there is no flag to make it configurable again, and you can no longer toggle enumerable or switch the property between a data and an accessor descriptor.
There are, however, two important exceptions while a property is non-configurable:
- You may change
writablefromtruetofalse(but not back fromfalsetotrue). - If the property is still
writable: true, you may change itsvalue— either by direct assignment or viaObject.defineProperty.
In other words, configurable: false locks the shape of the property, not necessarily its value. To truly freeze a property's value, set both configurable: false and writable: false.
Higher-Level APIs Built on These Flags
You rarely need to set flags one property at a time. JavaScript ships three built-in methods that flip these flags across a whole object:
Object.preventExtensions(obj)— stops new properties from being added. Existing properties can still be changed or deleted.Object.seal(obj)— prevents adding and deleting properties by marking every existing propertyconfigurable: false. Values can still change.Object.freeze(obj)— the strictest: seals the object and makes every propertywritable: false, so nothing can be added, removed, or changed.
Each method has a matching check: Object.isExtensible, Object.isSealed, and Object.isFrozen. Note that these operate one level deep — Object.freeze does not freeze nested objects (it is a "shallow" freeze).
Conclusion
Property flags and descriptors give you precise control over how object properties behave:
- A data descriptor pairs a
valuewithwritable; an accessor descriptor usesget/setfunctions instead. Both shareenumerableandconfigurable. - Read flags with
Object.getOwnPropertyDescriptor(one property) orObject.getOwnPropertyDescriptors(all own properties); write them withObject.definePropertyorObject.defineProperties. Inherited and missing properties returnundefined. configurable: falseis irreversible and locks the property's shape, though a still-writableproperty can have its value changed and itswritableflag turned off.- Violating a flag fails silently in non-strict mode but throws a
TypeErrorin strict mode. - Reach for
Object.freeze,Object.seal, andObject.preventExtensionswhen you want to lock an entire object instead of individual flags.
Next steps: dive into Property Getters and Setters for the accessor syntax, Object Methods and "this" for how this behaves inside them, and Prototypal Inheritance to see how property lookup walks the prototype chain.