Property descriptors, getters and setters
Property descriptors are a way to create (or modify) customized properties on objects. Generally, we create and use properties as follows:
// Specify a new property in object literal form
const myFavoriteBooks = {
fiction: 'Harry Potter',
};
// Access the property
console.log(myFavoriteBooks.fiction); // 'Harry Potter'
// Enumerate over object keys
console.log(Object.keys(myFavoriteBooks)); // ['fiction']
// Modify or write a new value
myFavoriteBooks.fiction = 'Sherlock Holmes';
console.log(myFavoriteBooks.fiction); // 'Sherlock Holmes'
// Delete or configure the property
delete myFavoriteBooks.fiction;
console.log(myFavoriteBooks.fiction); // undefined
In JavaScript, arrays are also objects. They have a length
property which returns the current length of the array.
While writing code, you might have noticed or run into some special cases where object properties behave differently. For example, consider an array of strings:
// Note: This code runs in non strict mode
const favoriteBooks = ['The Lord of the Rings'];
// Just getting the current length
console.log(favoriteBooks.length); // 1
favoriteBooks.push('Norwegian Wood');
// This updated automatically?
console.log(favoriteBooks.length); // 2
// Why can't I find length in the list of keys?
console.log(Object.keys(favoriteBooks)); // ['0', '1']
// Let me add another property to my array (object)
favoriteBooks.type = 'fiction';
// But the type property appears in the list
console.log(Object.keys(favoriteBooks)); // ['0', '1', 'type']
// Let me try deleting the length and type properties
delete favoriteBooks.length;
delete favoriteBooks.type;
// Cool, type property was removed
console.log(Object.keys(favoriteBooks)); // ['0', '1']
// Wait, why is the length property still there?
console.log(favoriteBooks.length); // 2
A few questions to think about after going through the above snippet:
- How does this
length
property always gives us the updated value of the length? It isn’t a function invocation likefavoriteBooks.length()
, but a regular property accessfavoriteBooks.length
. - We didn’t explicitly change its value (unlike the first example with
myFavoriteBooks.fiction
), yet it somehow was able to return the correct value. - The
length
property didn’t appear in the list of keys but the addedtype
property did. - Why did the
delete
operation fail?
The answers to all these questions would be easier to understand with property descriptors, getters and setters.
Property descriptors
Let’s take a step back to the first example.
const myFavoriteBooks = {
fiction: 'Harry Potter',
};
console.log(Object.getOwnPropertyDescriptor(myFavoriteBooks, 'fiction'));
// {
// value: 'Harry Potter',
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.getOwnPropertyDescriptor
returns property descriptor for the specified key (in this case fiction
).
Property descriptor are meta properties for object properties. They contain information on how the property behaves in certain scenarios and what all operations are permissible.
It’s also possible to create a new property with property descriptor using Object.defineProperty
:
const myFavoriteBooks = {};
Object.defineProperty(myFavoriteBooks, 'fiction', {
// Meta properties a.k.a property descriptor
value: 'Harry Potter',
// By default these are true
writable: true,
enumerable: true,
configurable: true,
});
console.log(myFavoriteBooks.fiction); // 'Harry Potter'
The meta properties can be broadly classified into two types:
- Data descriptors:
value
,writable
,enumerable
,configurable
- Accessor descriptors:
get
,set
,enumerable
,configurable
(often clubbed together as getter / setter pattern)
Data descriptor
Property descriptor behave a bit differently in non strict and strict mode. Some operations fail silently in non strict mode but throw a TypeError
in strict mode.
- value: defines the actual value held by the key.
- writable: when set to
true
, it allows changing the value of property. This fails silently in non strict mode but throw aTypeError
in strict mode. - enumerable: when set to
true
, it shows the property when enumerating over the object. For example infor
…in
andObject.keys
. -
configurable: when set to
true
, it allows changing the property descriptor definitions. For a non configurable property, this will throw aTypeError
. It has a couple of special cases:writable
can be changed fromtrue
tofalse
(but not reverse) even ifconfigurable
is set tofalse
configurable
also controls ifdelete
operation is permitted. If set tofalse
, thedelete
operation will fail silently in non strict mode but throw an error in strict mode
With this information, we can try to answer a few questions about .length
property on an array:
-
The
length
property didn’t appear in the list of keys but the addedtype
property did.- The
length
property must not beenumerable
. - By default any property added directly on the object is
enumerable
, thustype
appears when enumerating over keys.
- The
-
Why did the
delete
operation fail for.length
?- The
length
property must be nonconfigurable
.
- The
To answer the remaining questions lets look at property getters and setters.
Accessor descriptor
Accessor descriptor also have enumerable
and configurable
attributes that work same as data descriptor. The remaining two are:
- get: getter function for the specified property. Defaults to
undefined
. - set: setter function for the specified property. Defaults to
undefined
.
These lets us override the default internal operations that the JavaScript engine performs while setting or accessing properties.
let fiction = 'The Lord of the Rings';
const myBooksCollection = {
favorite: 'Harry Potter',
get fiction() {
// getter
return fiction;
},
set fiction(book) {
// setter
fiction = book;
},
};
// This accesses the property directly
console.log(myBooksCollection.favorite); // 'Harry Potter'
// This calls the getter function
console.log(myBooksCollection.fiction); // 'The Lord of the Rings'
// This calls the setter function
myBooksCollection.fiction = 'The Hobbit';
console.log(myBooksCollection.fiction); // 'The Hobbit'
// Also we can enumerate over the keys
console.log(Object.keys(myBooksCollection)); // ['favorite', 'fiction']
Whenever, a getter or setter function is called, its this
is bound to the object containing it. With this information, we can cover some pretty interesting usecases.
Building an arbitrary length property
The internal workings of length
property are pretty intricate (and hard to cover in this blog post) but we can try to build our own length
property to uncover some of the questions from earlier:
-
How does the
length
property always gives us the updated value of the array’s length?- It must be working like a getter function. Possibly, even without a function invocation like
length()
, using it as a propertylength
could be invoking an internal get function that returns the length.
- It must be working like a getter function. Possibly, even without a function invocation like
-
We didn’t explicitly change the value of
length
yet it somehow was able to return its correct (updated) value.- The internal workings depends on the JavaScript engine. However, lets try to implement something similar in a contrived example.
For this example, I’m using Object.defineProperties
which works similar to Object.defineProperty
but lets us define multiple properties at the same time.
// Need to add new books and maintain a list
//
// myBooksList.value → should return list of books
// myBooksList.value = 'ABC' → should add the book 'ABC' to the list
// myBooksList.length → should return the number of books in the list
const myBooksList = {};
Object.defineProperties(myBooksList, {
// underscore is used just as a convention
_value: {
value: [],
writable: false,
enumerable: false,
configurable: false,
},
value: {
// Note: a regular function instead of arrow function
// so this can be bound for getter / setter functions
get: function() {
return this._value;
},
set: function(book) {
this._value.push(book);
},
enumerable: true,
},
});
console.log(Object.keys(myBooksList)); // ['value']
myBooksList.value = 'Sapiens';
myBooksList.value = 'Born to Run';
console.log(myBooksList.value); // ['Sapiens', 'Born to Run']
Expanding upon the example to add a length
property:
const myBooksList = {};
Object.defineProperties(myBooksList, {
length: { value: 0, enumerable: false, configurable: false, }, _value: {
value: [],
writable: false,
enumerable: false,
configurable: false,
},
value: {
get: function() {
return this._value;
},
set: function(book) {
this._value.push(book);
this.length += 1; },
enumerable: true,
},
});
console.log(myBooksList.length); // 0
myBooksList.value = 'Animal Farm';
console.log(myBooksList.length); // 1
Voila! We just created our own length
property ✌️
Further reading
Personal blog of Divyanshu Maithani. I’m a software engineer working mostly on frontend. I also create programming videos with my friend. In my spare time I play music and DoTA.
You may follow me on twitter or join my newsletter for latest updates.