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 be writable.
  • 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/or configurable.