In JavaScript, object properties have two parts: a name and a descriptor. While the name is a string or symbol that identifies the property, the descriptor is an object that describes the configuration of the property. There are two types of property descriptor: data and accessor.
Getting a descriptor
To get a property descriptor, you can use the static Object.getOwnPropertyDescriptor()
method. You can also use the static Object.getOwnPropertyDescriptors()
method to get all property descriptors of a given object. Note that these are own property descriptors. Properties on an object’s prototype chain are not included.
Object.getOwnPropertyDescriptor(obj, prop);
Setting a descriptor
To set a property descriptor, you can use the static Object.defineProperty()
method. You can also use the static Object.defineProperties()
method to set multiple descriptors at once.
Object.defineProperty(obj, "prop", {
value: "foobar",
writable: true,
enumerable: true,
configurable: true,
});
If the property already exists, these methods modify it. The existing descriptor attributes are unmodified; only the ones you specify are changed.
If the property doesn’t exist, these methods define it. The possible descriptor attributes are as follows. They are all undefined
by default.
enumerable
configurable
value
(data descriptors only)writable
(data descriptors only)get
(accessor descriptors only)set
(accessor descriptors only)
If the descriptor doesn’t have a value
, writable
, get
, or set
attribute, it is treated as a data descriptor by default. If it has a combination of [value
or writable
] and [get
or set
] attributes, a TypeError
is thrown. It can’t be both types of descriptor.
Data descriptors
A data descriptor has a value
that may or may not be writable
. This is the type of property descriptor that is created when you add a property via an object initializer or through assignment. It is writable
by default when you add it in this way.
If you try to write to a read-only property in strict mode, you will get a TypeError
. It will fail silently otherwise. However, you can still delete it with the delete
operator.
Object.defineProperty(obj, "prop", {
value: "foo",
writable: false,
});
obj.prop = "bar"; // throws an error in strict mode
delete obj.prop; // ok
Accessor descriptors
An accessor descriptor is defined by a pair of functions: a getter and a setter. It’s a kind of pseudo-property. When the property is read, the getter is called; when it’s written, the setter is called.
let person = {
firstName: "Jane",
lastName: "Doe",
};
Object.defineProperty(person, "fullName", {
get() {
return this.firstName + " " + this.lastName;
},
set(value) {
[this.firstName, this.lastName] = value.split(" ");
},
});
You can also define an accessor descriptor by using the get
and set
syntax inside an object initializer.
let person = {
firstName: "Jane",
lastName: "Doe",
get fullName() {
return this.firstName + " " + this.lastName;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
},
};
When you define a getter without a setter, the property becomes read-only. If you try to write to the property in strict mode, you will get a TypeError
. It will fail silently otherwise. However, you can still delete it with the delete
operator, and the getter will be removed.
let person = {
firstName: "Jane",
lastName: "Doe",
get fullName() {
return this.firstName + " " + this.lastName;
},
};
person.fullName = "John Doe"; // throws an error in strict mode
delete person.fullName; // ok
Common attributes
The enumerable
and configurable
attributes are common to both types of property descriptor.
The enumerable
attribute
The enumerable
attribute controls whether a property appears during enumeration, e.g. in a for...in
loop or in a call to the Object.keys()
method.
let obj = {
prop1: "foo",
prop2: "bar",
};
Object.defineProperty(obj, "prop2", {
enumerable: false,
});
let keys = Object.keys(obj);
console.log(keys); // ["prop1"] <- "prop2" is not there
The configurable
attribute
The configurable
attribute controls whether a property can be configured. This includes whether its descriptor can be changed and whether it can be deleted. One exception is that if a property is writable
, it can still be made read-only even if it is not configurable.
let obj = {
prop: "foobar",
};
Object.defineProperty(obj, "prop", {
configurable: false,
}); // no longer configurable
Object.defineProperty(obj, "prop", {
writable: false,
}); // can still change from writable to read-only
Object.defineProperty(obj, "prop", {
enumerable: false,
}); // throws an error
delete obj.prop; // throws an error in strict mode
Summary
Next week, I’ll cover a handful of methods you can use to prevent an object from being modified, other than the Object.defineProperty()
method. Until then, here’s a summary of what we covered in this post.
- All object properties have a descriptor that describes their configuration.
- There are two types of descriptor: data and accessor.
- A data descriptor has a
value
that may or may not bewritable
. - An accessor descriptor is defined by a getter and a setter.
- A descriptor cannot be both types at the same time.
- When you add a property in “the normal way,” it is treated as a data descriptor.
- You can control whether any property is
enumerable
and/orconfigurable
.