Introduction

Prior to ECMAScript 3, there was not even a single way to tap into the internal attributes of a property of an object to configure the action made when the property was accessed i.e. retrieved or modified.

Since this was a useful feature, it eventually got introduced in edition 5 of the ECMAScript spec, released in December 2009. In addition to this feature, ES5 also changed the names of a couple of internal attributes related to properties. In this chapter, we shall see all of this in extreme detail.

So shall we begin?

What are property attributes?

As we saw in the previous chapter, an object is an unordered collection of properties. If we zoom into the term 'properties' in this definition, we see that it is quite diverse in its own meaning.

Recall that we said that a property is merely a characteristic, a trait, of an object. That is indeed true. The properties of all objects in JavaScript do describe some idea, some characteristic of the underlying object.

However, every property in JavaScript is associated with certain things that define characteristics of the property itself. We refer to these things as internal attributes of the property.

For instance, some properties can't be modified in JavaScript. In the spec, this characteristic of such a property is defined by the internal [[Writable]] attribute. If it's false, the property couldn't be modified. We'll explore [[Writable]] shortly below.

Now for each property, there are essentially a handful of these attributes. To encapsulate all these attributes for a given property, we have what's called a property descriptor.

As the name suggests,

A property descriptor is an object associated with a property that simply describes the property.

A property descriptor contains 'things' that tell us whether the property holds some data directly or are there some functions to access it; does it show in a for...in loop; can its value be changed; is it allowed to be configured; and so on and so forth.

As mentioned above, all these 'things' are given in the form of internal attributes.

Note that a property descriptor essentially describes the key of a property, NOT the corresponding value. For instance, if the descriptor says that the property's value couldn't be changed, this merely means that the key couldn't be assigned a different value; NOT that the actual value couldn't be changed/mutated.

According to the notational convention used by the spec, all internal attributes are represented by a pair of double square brackets ([[ ]]). These internal attributes are only for explanatory purposes i.e. to describe the internal behaviour of objects, explain algorithms for a given utility in the spec, etc.

Implementations of ECMAScript such as V8, SpiderMonkey, may use any way to model these internal attributes — they don't have to use (and actually none of them use) the [[ and ]] naming convention.

In fact, using [[ or ]] in the name of an identifier would anyways lead to an error in the programming language in which the engine of JavaScript is written — usually C++. So the idea of actually using [[ or ]] becomes pointless in the actual implementation anyway.

Coming back to property descriptors, there are essentially two kinds of descriptors:

  1. Data property descriptors
  2. Accessor property descriptors

To understand these, we first need to understand the two kinds of properties in JavaScript: data properties and accessor properties.

Data and accessor properties

Most of the object properties that we have been working with so far in this course were data properties.

A data property is directly associated with a corresponding value.

When the property is read, the corresponding value is returned. Similarly, when a value is set on the property, the value is assigned as it is to the property — there is just nothing in between the retrieval or between the assignment.

It's called a 'data' property because it directly contains the underlying data.

Data properties have two internal attributes that classify them as a data property: [[Value]] and [[Writable]].

  1. [[Value]] — simply contains the value associated with the given property.
  2. [[Writable]] — a Boolean that specifies whether it's allowed to assign a new value to the property or not, which is commonly known as 'writing to the property'.

The second kind of property in JavaScript is called on an accessor property.

An accessor property is associated with functions that are called when the property is read or written to.

When the property is read, a function is called the getter is invoked. Similarly when the property is written to, the setter function is invoked.

The name 'accessor' refers to the fact that the property accesses a piece of data via a function — it doesn't directly contain the data (as does a data property).

Accessor properties define at least one accessor function by virtue of the following attributes:

  1. [[Get]] — specifies a zero-arg function, known as the getter, to be called when the property is read.
  2. [[Set]] — specifies a one-arg function, known as the setter, to be called when the property is assigned a value.

So to reiterate it, data properties defines the attributes [[Value]] and [[Writable]] while accessor properties define the attributes [[Get]] and [[Set]].

A data property can't have a [[Set]] or [[Get]] attribute likewise an accessor property can't have a [[Value]] or [[Writable]] attribute.

Now apart from these two sets of attributes, a property could have two other attributes as described below:

  1. [[Enumerable]] — a Boolean specifying whether the property shows up in a for..in loop.
  2. [[Configurable]] — a Boolean specifying whether the property's attributes could be changed or the property be deleted via the delete keyword.

With all this brief information about property attributes in mind, it's time to take each section and explore it in even more detail.

First, let's talk about the new method Object.defineProperty() intercalated in ECMAScript 5 to allow developers to configure property attributes of custom objects and, in some cases, of predefined objects as well.

The Object.defineProperty() method

Introduced in ECMAScript 5, the Object.defineProperty() method allows us to create a property on an object with given internal attributes.

The syntax of the method is quite intuitive:

Object.defineProperty(object, propertyName, propertyDescriptor)

The first object argument specifies the object on which to define the property, the second propertyName argument specifies the name of this property as a string, and finally the third argument specifies the property's descriptor object.

Quite easy to pick up, isn't this?

First the object, then the property's name, then the descriptor — that's it!

The propertyDescriptor object could have the following properties:

  1. value — specifes a value for the [[Value]] internal attribute.
  2. writable — specifes a Boolean for the [[Writable]] internal attribute.
  3. get — specifes a zero-arg function for the [[Get]] internal attribute.
  4. set — specifes a one-arg function for the [[Set]] internal attribute.
  5. enumerable — specifes a Boolean for the [[Enumerable]] internal attribute.
  6. configurable — specifes a Boolean for the [[Configurable]] internal attribute.

Specifying a property other than the ones shown above on the property descriptor object would simply be ignored.

Note that if the get and/or set properties are mentioned, then it's an error to set writable or value on the descriptor object.

As you can see, the nomenclature of all the properties shown above is synonymous to the nomenclature of the internal attributes with mainly one difference — the first letter of these properties is in lowercase.

Attributes of data properties

As we saw above, there are two attributes specific to a data property: [[Value]] and [[Writable]]. These correspond with the properties value and writable of the descriptor object passed to Object.defineProperty().

Let's consider a handful of examples to illustrate these attributes.

Below we create a simple object obj with two properties a and b on it just as we are used to from the previous chapters.

var obj = { a: 'old' };
obj.b = 'old';

Properties created in these normal ways are all data properties and have their [[Writable]] attribute set to true by default, and [[Value]] set to the given value ('old' in the case above).

[[Writable]] being true is the reason why an expression like obj.a = 'new' has an actual consequence.

Let's go on, assign a new value to obj.a and obj.b and see whether the value both these properties are actually changed:

var obj = { a: 'old' };
obj.b = 'old';

console.log(obj.a, obj.b);

obj.a = 'new';
obj.b = 'new';

console.log(obj.a, obj.b);
old old
new new

As can be seen, it actually does change.

Now, let's go on and define a read-only property c on the same obj object:

var obj = { a: 'old' };
obj.b = 'old';

Object.defineProperty(obj, 'c', {
    value: 'old',
    writable: false
});

console.log(obj.c);
old

In line 4, we call Object.defineProperty() to define a property c on obj with its value set to 'old' and its [[Writable]] attribute set to false via the writable property of the given descriptor object. This should have the effect of any write operation made to this property to simply fail.

Let's try writing a new value to this property.

var obj = { a: 'old' };
obj.b = 'old';

Object.defineProperty(obj, 'c', {
    value: 'old',
    writable: false
});

console.log(obj.c);

obj.c = 'new'; console.log(obj.c);
old
old

As is evident by the console's output, the value of obj.a is 'old' even after the assignment in line 4. This confirms the fact that setting a property on an object via Object.defineProperty() with writable set to false effectively prevents overwriting the defined value of that property.

Let's also try redefining the property via the same Object.defineProperty() method and see whether it works or not:

var obj = { a: 'old' };
obj.b = 'old';

Object.defineProperty(obj, 'c', {
    value: 'old',
    writable: false
});

console.log(obj.c);

// let's try to redefine obj.c
Object.defineProperty(obj, 'c', {
value: 'new',
writable: false
}); console.log(obj.c);
Uncaught TypeError: Cannot redefine property: c at Function.defineProperty (<anonymous>) at <anonymous>:12:8

Clearly no! We can't even redefine the property.

Great!

Actually the error thrown above is because of the internal [[Configurable]] attribute set to false, not because of [[Writable]] set to false. We'll see this distinction in the section on [[Configurable]] below.

Anyways, coming back to data properties, omitting value in a data property descriptor results in [[Value]] being set to undefined (since [[Value]] merely retrieves propertyDescriptor.value, which would resolve to undefined when value is absent).

This can be seen as follows:

var obj = { a: 'old' };
obj.b = 'old';

Object.defineProperty(obj, 'c', {
    // value: 'old',
    writable: false
});

console.log(obj.c);
console.log('c' in obj);
undefined
true

The first log confirms the fact that the property c has the value undefined. The second log, however, tells us that the object obj does contain the property x, it's just that its value is undefined.

Attributes of accessor properties

Accessor properties use accessor functions to retrieve and set a given piece of data. One is called a getter while the other is called the setter.

For a property to be termed an accessor property, it must have at least one of these accessor functions. The getter goes in the property's [[Get]] attribute whereas the setter goes in the property's [[Set]] attribute.

The corresponding properties of the description object passed to Object.defineProperty() for [[Get]] and [[Set]] are get and set, respectively.

Let's create an accessor property.

Below, we have our old point object from the previous chapter. We'll create an accessor property distanceFromOrigin on it to get the distance of the point from the origin:

var point = { x: 0, y: 0 };

Object.defineProperty(point, 'distanceFromOrigin', {
    get: function() {
        return (this.x ** 2 + this.y ** 2) ** 0.5;
    }
});

Now, let's retrieve this property:

var point = { x: 0, y: 0 };

Object.defineProperty(point, 'distanceFromOrigin', {
    get: function() {
        return (this.x ** 2 + this.y ** 2) ** 0.5;
    }
});

point.x = 4;
point.y = 3;

console.log(point.distanceFromOrigin);
5

Voila!

We get the distance returned as if it was actually stored in distanceFromOrigin, however it's computed on the fly.

Notice that there is no need to put a pair of parentheses after the property's read expression point.distanceFromOrigin. In fact, doing so would be an error. We'll see why shortly below.

Moving on, if you have a good understanding of graphs and geometry, you'll know that it's completely impossible to determine the co-ordinates of a point, given its distance from the origin — there are just an infinite amount of possibilities!

Hence, in the case above, it seems that manually setting distanceFromOrigin is senseless and should, likewise, throw an error to alert the developer of the erroneous set operation.

But how do we accomplish this?

Well, simply by setting up a setter function.

Consider the code below:

var point = { x: 0, y: 0 };

Object.defineProperty(point, 'distanceFromOrigin', {
    get: function() {
        return (this.x ** 2 + this.y ** 2) ** 0.5;
    },
    set: function(value) {
        throw new Error("Can't set distanceFromOrigin!");
    }
});

In addition to the getter, now we even define a setter. The function simply throws an error saying that distanceFromOrigin can't be set, when we have a set operation for the property!

Let's try it out:

var point = { x: 0, y: 0 };

Object.defineProperty(point, 'distanceFromOrigin', {
    get: function() {
        return (this.x ** 2 + this.y ** 2) ** 0.5;
    },
    set: function(value) {
        throw new Error("Can't set distanceFromOrigin!");
    }
});

point.distanceFromOrigin = 100;
Uncaught Error: Can't set distanceFromOrigin! at Object.set (<anonymous>:8:13) at <anonymous>:12:26

Perfect!

One thing to keep in mind is that the setter function takes in one arg when called. This is the value assigned to the property in the property's assignment expression.

Let's consider an example using a more meaningful setter function.

Suppose that we are coding an online shopping store in JavaScript. Each item is represented by an object with numerous properties on it. For now, to keep things simple, let's just focus on the two properties sellingPrice and discountPrice.

sellingPrice specifies the price at which the item was originally being sold in the shopping store while discountPrice specifies the price after applying a discount on the product.

Now, while developing this application, let's say that we feel that a discount property would be required as well to specify the discount offered on the item, as a percentage value.

Obviously creating a data property would be redundant because, given the sellingPrice and the discountPrice, the discount can be easily determined by a bit of elementary math applied over these two values.

Hence, we choose to go with an accessor property discount for every item.

Creating an accessor property for every item in this shopping store's application is quite inefficient in terms of memory. As we shall see in the following chapters, there are far better ways to do this — define the accessor property once on the single prototype object of all the item objects, which are themselves instances of the Item() constructor. We'll explore all these useful ideas in the next chapters.

Below we have modeled one item of the shopping store using an object literal:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Now, let's define the discount accessor property for this item:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'discount', {
    get: function() {
        return Math.round(
            (this.sellingPrice - this.discountPrice)
            / this.sellingPrice * 100
        );
    }
});

console.log(item.discount + '% discount offered.');
40% discount offered.

If you think about it, it's possible to determine the value of discountPrice when a value is assigned to discount, given that we assert that sellingPrice remains is fixed.

For instance, if initially the sellingPrice is 100 and the discountPrice is 80, then discountPrice would become 50 (and sellingPrice would remain 100).

Here, we assume that sellingPrice is fixed, because if this was not the case, there would have been one more possibility i.e. sellingPrice becomes 160 while discountPrice remains 80.

Likewise, let's also define a setter for discount:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'discount', {
    get: function() {
        return Math.round(
            (this.sellingPrice - this.discountPrice)
            / this.sellingPrice * 100
        );
    },
    set: function(value) {
        this.discountPrice = this.sellingPrice
                                    - (this.sellingPrice * value / 100);
    }
});

It's all just a bit of math in action, that's it!

With this accessor property set, why not go ahead and test it:

item.discount = 20
20
item.discount
20

Everything works perfectly. Superb!

As stated before, for any property to be considered an accessor property, it must have at least one of the accessor functions, i.e. a getter or a setter, defined.

If only one is specified, the other is set as undefined, and henceforth, operations that involve this undefined value would either return undefined (in the case of an undefined getter) or be ignored (in the case of an undefined setter).

What's important to note is that in either way, there is NO error thrown, which ideally should've been the .

Enumerability

Regardless of the kind of property being defined, two attributes can always be set for that property. They are [[Enumerable]] and [[Configurable]].

In this section, our aim is to shed some light over the former i.e the [[Enumerable]] attribute. Let's start.

As said before, the [[Enumerable]] attribute specifies whether a property is enumerable. But what exactly does it mean for a property to be enumerable?

Well:

An enumerable property is one that shows up in a for...in loop.

And that's just it.

If a property is enumerable, it will show up in for...in, and if it ain't enumerable, it won't show up in for...in.

Simple!

The enumerable property of the descriptor object passed to Object.defineProperty() specifies a value for the [[Enumerable]] attribute. If false, the corresponding property being set of the object is non-enumerable, and if true, the property is enumerable.

Let's consider an example.

Following we have our same old code from the section above.

var point = { x: 0, y: 0 };

Object.defineProperty(point, 'distanceFromOrigin', {
    get: function() {
        return (this.x ** 2 + this.y ** 2) ** 0.5;
    },
    set: function(value) {
        throw new Error("Can't set distanceFromOrigin!");
    }
});

Since enumerable isn't specified in the descriptor above, it's taken as false. Hence, if we use point with a for...in loop, the property distanceFromOrigin won't show up.

The code below demonstrates this:

/* ... */

for (var prop in point) {
    console.log(prop);
}
x
y

If we want this property to show up in for...in then we ought to set enumerable to true when defining distanceFromOrigin.

This is exactly what we do below:

var point = { x: 0, y: 0 };

Object.defineProperty(point, 'distanceFromOrigin', {
    get: function() {
        return (this.x ** 2 + this.y ** 2) ** 0.5;
    },
    set: function(value) {
        throw new Error("Can't set distanceFromOrigin!");
    },
enumerable: true });

Let's now rerun the for...in loop over this new object.

/* ... */

for (var prop in point) {
    console.log(prop);
}
x
y
distanceFromOrigin

As expected, now the property shows up in the loop.

Perfect!

Configurability

The last attribute left to be discovered in this chapter is [[Configurable]]. It flags a property as configurable. And what does this mean?

Well,

A configurable property is one that could be deleted and whose attributes could be changed.

For instance, if a property x on an object o is configurable, then its [[Enumerable]] attribute could be changed from true to false, and vice versa, or it could be deleted via the expression delete o.x.

As with all other attributes, the corresponding property on the descriptor object for the [[Configurable]] attribute is configurable.

If true, the underlying property is configurable and if false, the underlying property is non-configurable. Quite rudimentary, isn't it?

Moving on, contrary to what one might expect, going the other way round, a non-configurable property does not imply that none of its attributes could be changed — there might be a few of them that could be changed.

For instance, if a property x on an object o is non-configurable but is writable, then it's possible for its [[Value]] to be changed to any other value. This is a fairly sensible decision — the property is writable and therefore should be able to register new value assignments.

Following the norm, it's example time.

Consider the code below:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'id', {
    value: 1,
    configurable: false
});

We define a property id on the object item with its value set to 1, and configurable set to false. Since writable isn't specified in the descriptor object, it's assumed to be false.

Hence, a write operation on item.id would have no effect, as shown below:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'id', {
    value: 1,
    configurable: false
});

item.id = 'new';

console.log(item.id);
1

Even, redefining the property via a second call to Object.defineProperty() would fail; in fact, it would throw an error:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'id', {
    value: 1,
    configurable: false
});

Object.defineProperty(item, 'id', {
    value: 'new',
    configurable: false
});

console.log(item.id);
Uncaught TypeError: Cannot redefine property: id at Function.defineProperty (<anonymous>) at <anonymous>:11:8

Now, as we stated above, this error is not caused because of [[Writable]] set to false, but rather because of both [[Writable]] set to false and [[Configurable]] set to false.

If we set configurable to true on the property descriptor object passed to the first call of Object.defineProperty() above while keeping writable as false, it would be possible to assign a new value to id via a second call to Object.defineProperty().

Consider the following code. It's the same as the second code snippet (shown above) in this section with the exception of the configurable property — it's now set to true:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'id', {
    value: 1,
configurable: true }); item.id = 'new'; console.log(item.id);
1

In line 11, we aim to write a new value to the property id and then log it, in line 13. As expected, since the [[Writable]] attribute of id is false (because writable is omitted from the descriptor object above) it's not possible to change id via a property assignment expression.

So with [[Configurable]] set to true, and [[Writable]] still false, a property can't be written to by means of a normal assignment expression.

Let's try the second option — redefining id via a second call to Object.defineProperty().

Consider the code below. Here we redefine id by calling Object.defineProperty() a second time, and then log item.id to see whether it's changed:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'id', {
    value: 1,
    configurable: true
});

Object.defineProperty(item, 'id', {
value: 'new',
configurable: true
}); console.log(item.id);
new

Quite surprisingly, as can be seen in the console, the property has changed!

The [[Writable]] attribute of id is still false, nonetheless it didn't prevent id from being assigned a new value via another call to Object.defineProperty() above.

How is this possible?

Well, it's a sensible decision made by the standardising committee of ECMAScript to allow redefining a non-writable, configurable property via another call to Object.defineProperty().

Let's see how...

In the code above, configurable: true means that the value of any attribute of id could be changed to some other value. Assuming that redefining a non-writable configurable property was considered an error (which currently isn't, as we've seen above) any developer could've done the following to by-pass it:

var item = {
    sellingPrice: 100,
    discountPrice: 60
};

Object.defineProperty(item, 'id', {
    value: 1,
    configurable: true
});

// the value of id isn't changed here, likewise nothing new is written
// so no error is thrown
// however, [[Writable]] is set to true,
// allowing for subsequent write operations
Object.defineProperty(item, 'id', {
    value: 1,
    writable: true,
    configurable: true
});

// a new value is written to id, since [[Writable]] is true
// likewise no error is thrown
// now the property is made non-writable again
Object.defineProperty(item, 'id', {
    value: 'new',
    configurable: true
});

Watch closely what's happening here — the comments are there to help understand what each call to Object.defineProperty() is doing.

Let's see it all more closely:

  1. In the first call to Object.defineProperty() (in line 6), we set a property id on item with the value 1, [[Writable]] set to false (since it's omitted) and [[Configurable]] set to true as well (due to the configurable: true property).
  2. This would have meant that writing a new value to id, by a simple assignment expression such as item.id = 2, or by another call to Object.defineProperty(), would cause an error due to [[Writable]] being false.
  3. But at the same time, this would have also meant that changing the value of any other attribute such as [[Enumerable]] or [[Writable]] is allowed, or even to convert the property into an accessor property.
  4. So, what's done to by-pass this is that first the property id is redefined using Object.defineProperty() (in line 15), keeping [[Value]] the same but changing writable to true.
  5. If the [[Value]] of id was changed in this call to Object.defineProperty() an error would have been thrown, since [[Writable]] was false initially on id's descriptor.
  6. Anyways, after the second call to Object.defineProperty() is complete, id becomes writable.
  7. Following this, Object.defineProperty() is called once more (in line 24) to change the [[Value]] of id.
  8. All other attributes of id are reset to their original values that they had after the first call to Object.defineProperty() (in line 6).
  9. In the code above, this is simply done by merely copy pasting the first call to Object.defineProperty() from line 6 to line 24, and adding the new value next to the value property i.e. the string 'new'.
  10. Once this third call to Object.defineProperty() reaches completion, id is non-writable and configurable — just as it was after its first definition (in line 6) — however, now it has a new value.

Isn't this quite clever?

Well, it sure is.

And the ECMAScript standardising committee foresaw this kind of a hack and therefore allowed writing to a non-writable configurable property.

However, if the property is a non-writable non-configurable property, then there is just no chance of being able to use anything to change its value.

Let's see all the things brought about with a false [[Configurable]] attribute:

  1. The value of the [[Enumerable]] attribute can't be changed.
  2. The value of the [[Configurable]] attribute can't be changed to true.
  3. The underlying property can't be deleted via the delete keyword.
  4. The underlying property can't be converted from a data property to an accessor property or from an accessor property to a data property.

Shown below are all these things:

Firstly, the value of the [[Enumerable]] attribute can't be changed:

var obj = {};

Object.defineProperty(obj, 'a', {});

Object.defineProperty(obj, 'a', {
    enumerable: true
});
Uncaught TypeError: Cannot redefine property: a at Function.defineProperty (<anonymous>) at <anonymous>:5:8

When defining id for the first time, its value is set to undefined while its [[Writable]], [[Enumerable]] and [[Configurable]] attributes are all set to false, as they are all omitted.

This means that the property is non-configurable, and therefore, changing the [[Enumerable]] attribute would throw an error, which is exactly what happens in the code above.

Moving on, secondly, the value of the [[Configurable]] attribute can't be changed to true for a non-configurable property:

var obj = {};

Object.defineProperty(obj, 'a', {});

Object.defineProperty(obj, 'a', {
    configurable: true
});
Uncaught TypeError: Cannot redefine property: a at Function.defineProperty (<anonymous>) at <anonymous>:5:8

Thirdly, a non-configurable property can't be deleted:

var obj = {};
Object.defineProperty(obj, 'a', {});

delete obj.a;

console.log('a' in obj);
true

After deleting the property in line 4, when we check whether there exists a property 'a' on obj using the in operator, we get back true returned, which confirms that the property is still there and that the deletion didn't go successful.

In strict mode, the deletion throws an error:

'use strict';

var obj = {};
Object.defineProperty(obj, 'a', {});

delete obj.a;
Uncaught TypeError: Cannot delete property 'a' of #<Object> at <anonymous>:6:1

Moreover, a non-configurable data property can't be transformed into an accessor property, and similarly a non-configurable accessor property can't be transformed into a data property:

var obj = {};

// obj.a is a data property
Object.defineProperty(obj, 'a', {});

// make it an accessor property
Object.defineProperty(obj, 'a', {
    get: function() {}
});
Uncaught TypeError: Cannot redefine property: a at Function.defineProperty (<anonymous>) at <anonymous>:7:8

Here, we demonstrate the transformation of a non-configurable data property to an accessor property; you could try the same for a non-configurable accessor property.

One last thing left to be discussed in regards to non-configurable data properties is detailed below.

Non-configurable data properties and writability

If a data property is non-configurable, i.e. has its [[Configurable]] attribute set to false, being able to change its [[Value]] and [[Writable]] attributes depends on the value of [[Writable]].

If the property is writable, i.e. its [[Writable]] attribute is true, this simply means that the property could be assigned a new value.

Hence, sensibly, the non-configurable nature of the property together with its writable nature does not disallow redefining it via another call to Object.defineProperty() and then changing the value of the [[Value]] attribute (while keeping all other attributes set to their original values).

In other words, it's possible to change the value of a non-configurable property, i.e. change the value of its [[Value]] attribute, given that its [[Writable]] attribute is true.

In addition to this, we could even change the [[Writable]] attribute of a non-configurable property from false to true, but NOT the other way round.

This is allowed since it makes the property even more strict in its operations — previously the property could be assigned a new value but now it couldn't.

Only those changes are prevented that make a property less strict in its operation. For instance, it's not possible to change the [[Writable]] attribute of a non-configurable property from false to true because, otherwise, it would allow writing any new value to the property, which makes it less strict that a non-configurable non-writable property.

Ideally, the job of the ECMAScript committee is to allow for features that don't make a given feature useless. In allowing changing [[Writable]] from true to false for a non-configurable property, all the non-configurability of the property is preserved — the [[Configurable]] attribute set to false isn't useless.

In fact, it becomes even more non-configurable as now we couldn't even change the [[Value]] and [[Writable]] attributes.

However, if a non-configurable data property is additionally non-writable as well, i.e. its [[Writable]] attribute is false, it means that the property couldn't be assigned a new value.

Hence, once again, sensibly this non-configurable nature of the property, together with its non-writable nature, disallows changing its [[Value]] attribute to any other value or its [[Writable]] attribute to true.

How to remember all of this?

No doubt in the fact that there is just a lot of information above to digest in one single go.

At this point, if you feel that you couldn't remember even a single thing what's stated above such as when could we change [[Writable]] to true from false, or whether we could redefine [[Set]] for a non-configurable property, then here's a simplistic approach to remember everything.

Property attributes sound easier than read. They are an extremely simple concept and remembering how they work shouldn't be difficult at all!

Yup, that's right.

In the previous sections, we had to explain each and every aspect of all the given property attributes, each of which encompasses multiple cases. Showing all these cases along with the code snippets leads to quite long and cluttered text.

If we consolidate everything together, it might seem way more easier to digest how property attributes work.

Let's do it...

  1. When a property is writable (regardless of the value of other attributes), we could change its [[Value]] manually or via a property assignment expression, for e.g. o.p = 10. Simple!
  2. When a property is non-writable (regardless of the value of other attributes), we could not change its [[Value]] via a property assignment expression, for e.g. o.p = 10. Simple!
  3. When a property is configurable and writable, essentially all operations are possible on the property. Simple!
  4. When a property is configurable but non-writable, we couldn't use a property assignment expression, for e.g. o.p = 10, to change its value. This follows from 2). However, all other operations are still possible on the property including manually changing [[Value]] using Object.defineProperty(). This follows from 3). Simple!
  5. When a property is non-configurable and an accessor property, no attribute could be changed. Simple!
  6. When a property is non-configurable and non-writable, no attribute could be changed. Simple!
  7. When a property is non-configurable but writable, no attribute could be changed except for [[Value]], as mentioned in 1), and [[Writable]], to make the property even more strict. Simple!

And this ends this sufficiently long discussion.