With the properties of objects, you can set more than just their values. In this article, you can learn what is the property descriptor and what you can achieve with it. Among other things, you can use getters and setters and the article also explains their concepts.
Property descriptor
A descriptor is a structure that holds information about the property. To better illustrate it, let’s use an example.
1 2 3 4 5 |
const dog = { name: 'Fluffy' }; Object.getOwnPropertyDescriptor(dog, 'name'); |
1 2 3 4 5 6 |
{ value: 'Fluffy', writable: true, enumerable: true, configurable: true } |
The getOwnPropertyDescriptor function returns a descriptor of a provided object. As the name suggests, it does so only for the own properties. That means it only works for the properties directly owned by the object, and not just simply in the prototype chain.
If you would like to know more about prototypes and the OOP in JavaScript, check out Prototype. The big bro behind ES6 class
Configurable
This boolean determines if you can configure an existing descriptor. It also decides if the property can be deleted with the delete keyword.
1 2 3 4 5 |
const dog = {}; Object.defineProperty(dog, 'name', { value: 'Fluffy', configurable: false }); |
The defineProperty function creates a new property of an object or modifies an existing one.
Trying to delete non-configurable property results in an error in strict mode. It is also worth noticing that if you create a property using a standard assignment, configurable equals true, but with the defineProperty the default value is false.
Enumerable
This property is a boolean determining if the property should show up during enumeration. An example of such is the for in loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const container = {}; container.one = 'one'; container.two = 'two'; container.three = 'three'; Object.defineProperty(container, 'three', { enumerable: false }); for(let property in container){ console.log(property); } // one // two |
This property is true by default if you use standard assignment such as the container.one = 'one', but defaults to false when the defineProperty is used.
Data descriptors
There are two types of descriptors and the data descriptor is one of them. It is able of holding the configurable and enumerable properties and more. If you use the assignment operator to create a property, you give it the data descriptor.
Value
The value of the property.
Writable
The writeable property holds a boolean that indicates if the value can be changed. If you are in the strict mode, you cause an error to be thrown when you try to change the value of a non-writeable property. It defaults to true if you use regular assignment and false if you use defineProperty. An interesting fact is that even if configurable is false, you can change the writeable property from true to false. It is due to some legacy reasons.
Accessor descriptors
The other type of descriptors are the accessor descriptors. They also have the configurable and enumerable properties.
Get
It is a function that is called when a property is being accessed. The return of that function is treated as the value of the property.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const date = { day: 10, month: 11, year: 2018 }; Object.defineProperty(date, 'fullDate', { get: function () { return `${this.day}.${this.month}.${this.year}` }, enumerable: true, configurable: true }); console.log(date.fullDate); // '10.11.2018' |
You can remove the getter with the delete keyword.
An interesting use-case is the usage of self-overwriting getters (also called lazy getters). Let’s start with an example:
1 2 3 4 5 6 |
Object.setPropertyOf(user, 'avatar', { get: function(){ delete this.avatar; return this.avatar = document.querySelector('#avatar'); } }) |
In the first line, the getter deletes itself. On the second one, it both assigns a data property to the object and returns it. Just assigning the value wouldn’t be enough because the getter would still be there and would be called when you access the property again.
When you look closely you can see that the value is calculated only when the value is accessed for the first time. If the value turns out not to be needed, it will never be calculated at all. It might be useful if it takes a lot of time. Remember not to use lazy getters if you expect the value to ever change!
Set
The setter is a function called when a property is assigned a value. The assigned value is passed to the function as a parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const configuration = { log: [] } Object.defineProperty(configuration, 'primaryColor', { set: function(newColor){ this._primaryColor = newColor; this.log.push(newColor); }, get: function() { return this._primaryColor; } }); |
A setter needs to be a function with exactly one parameter. With code like the one above, you can easily implement the redo functionality. If you wonder why we don’t just write this.primaryColor = newColor, it would actually cause the setter to be called over and over, resulting in exceeding the maximum call stack size! You can remove the setter with the delete keyword.
There is also another syntax which you can use to define getters and setters.
1 2 3 4 5 6 7 8 9 10 |
const configuration = { history: [], set primaryColor(newColor) { this._primaryColor = newColor; this.history.push(newColor); }, get primaryColor(){ return this._primaryColor; } } |
You might find it especially convenient with the usage of ES6 classes.
An important thing to keep in mind when defining properties is that descriptor can’t be both a data descriptor and an accessor descriptor. If you define a getter or a setter for a property that has the data descriptor and value, it will be removed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const date = { day: 10, month: 11, year: 2018, fullDate: '10.11.2018' }; Object.getOwnPropertyDescriptor(date, 'fullDate').value; // '10.11.2018' Object.defineProperty(date, 'fullDate', { get: function () { return `${this.day}.${this.month}.${this.year}` }, enumerable: true, configurable: true, }); Object.getOwnPropertyDescriptor(date, 'fullDate').value; // undefined |
It works both ways: if you assign a value using the defineProperty, you remove the getter and the setter.
Summary
In this article, we’ve covered the property descriptors. It included two types of them: data descriptors and accessor descriptors. Even though they share some common properties, they are different. The article covered both the properties they share (such as enumerable) and the properties that are unique for a certain type of a descriptor. The knowledge of them is certainly useful and can both simplify your code (with the usage of getters and setters) and help you understand the JavaScript better since some of the objects like the window use descriptors.