Lesson 04 / 17
Property Descriptors
The writability, enumerability, and configurability flags a property carries alongside its value; accessor properties and copying that preserves descriptors.
Contents
The previous lesson arrived at the same place twice: class methods do not appear in
for...in output, manually written assignments do; and reading, though written to look
like a read, ran a function. Both are the result of a single fact.
A property is not made up of just a “name → value” pair. Alongside every property stands a property descriptor that determines its behavior. This lesson opens the descriptor’s fields one by one, then takes up accessor properties, which carry a function in place of a value.
Reading the Descriptor
Object.getOwnPropertyDescriptor returns the descriptor of an object’s own property.
Data properties carry four fields: value, writable, enumerable, configurable.
function flags(obj, name) { const descriptor = Object.getOwnPropertyDescriptor(obj, name); if (descriptor === undefined) return `${name}: no descriptor`; if ("value" in descriptor) { return `${name}: data, writable=${descriptor.writable}, enumerable=${descriptor.enumerable}, configurable=${descriptor.configurable}`; } return `${name}: accessor, getter=${typeof descriptor.get}, setter=${typeof descriptor.set}, enumerable=${descriptor.enumerable}`; } class MeasurementRecord { constructor(sensor, value) { this.sensor = sensor; this.value = value; } format() { return `${this.sensor}: ${this.value}`; } } const record = new MeasurementRecord("S-01", 21.4); const plainRecord = { sensor: "S-02", value: 19.8 }; Object.defineProperty(plainRecord, "unit", { value: "celsius" }); console.log(flags(record, "sensor")); console.log(flags(MeasurementRecord.prototype, "format")); console.log(flags(plainRecord, "sensor")); console.log(flags(plainRecord, "unit")); console.log(flags(record, "format"));
sensor: data, writable=true, enumerable=true, configurable=true format: data, writable=true, enumerable=false, configurable=true sensor: data, writable=true, enumerable=true, configurable=true unit: data, writable=false, enumerable=false, configurable=false format: no descriptor
Three rules follow. Properties produced by ordinary assignment or an object literal have
all three flags open. Class methods are defined as non-enumerable — this is the source of
the for...in difference from the previous lesson. In an Object.defineProperty call,
every flag not specified is counted as closed; this is why the unit property comes
out non-writable, non-enumerable, and non-configurable. The last line is a reminder of the
word “own” in the function’s name: because format stands on the prototype, the record
has no definition of it on itself.
Writability
If the writable flag is closed, assignment does not change the value. There are two
different forms of not changing, and the difference depends on whether the code runs in
strict mode.
const record = { sensor: "S-01", value: 21.4 }; Object.defineProperty(record, "unit", { value: "celsius", writable: false, enumerable: true, configurable: false, }); console.log(record.unit); try { record.unit = "fahrenheit"; } catch (error) { console.log(`${error.constructor.name} caught`); } console.log(record.unit); function nonStrictWrite(obj) { return new Function("n", "n.unit = 'fahrenheit'; return n.unit;")(obj); } console.log(nonStrictWrite(record)); try { Object.defineProperty(record, "unit", { value: "kelvin" }); } catch (error) { console.log(`${error.constructor.name} caught`); } console.log(record.unit);
celsius TypeError caught celsius celsius TypeError caught celsius
Module files and class bodies run in strict mode; there, assigning to a non-writable
property throws an error. The body produced with new Function is not in strict mode, so
the same assignment silently fails — the unchanged value on the fourth line shows this.
Because silent failure lets an error be noticed long after the assignment, strict mode is
preferred.
The last two lines are the effect of the configurable flag. Once configurability is
closed, the descriptor can never be changed again and the property cannot be deleted;
this is the one flag that cannot be reversed. For this reason, configurable: false
should be a deliberate decision.
Enumerability
The enumerable flag determines whether a property enters listings. Its effect spreads
across a wide set of operations.
const record = { sensor: "S-01", value: 21.4 }; Object.defineProperty(record, "sourceFile", { value: "measurement-2.csv", writable: true, enumerable: false, configurable: true, }); console.log(record.sourceFile); console.log(Object.keys(record).join(",")); console.log(Object.getOwnPropertyNames(record).join(",")); console.log(JSON.stringify(record)); console.log(JSON.stringify({ ...record })); console.log(JSON.stringify(Object.assign({}, record))); console.log("sourceFile" in record); console.log(Object.entries(record).length);
measurement-2.csv
sensor,value
sensor,value,sourceFile
{"sensor":"S-01","value":21.4}
{"sensor":"S-01","value":21.4}
{"sensor":"S-01","value":21.4}
true
2
The property is readable and the in operator sees it; but Object.keys,
Object.entries, JSON.stringify, object spread, and Object.assign skip it.
Object.getOwnPropertyNames does not look at enumerability, which is why it is the way
to inspect properties that stay hidden.
A practical rule follows from this: information that accompanies data without being
part of the data — a source file name, internal counters, cache fields — when defined
as non-enumerable, does not automatically get caught up in serialization and copying
operations. Symbol-keyed properties give the same result too; they also do not enter
Object.keys and JSON.stringify output.
Accessor Properties
The second descriptor kind carries get and set functions in place of value and
writable. A property like this is called an accessor: reading runs the get
function, writing runs the set function.
An accessor’s value is presenting a computed magnitude as if it were a property. In a measurement record, the read value is the sum of the raw value and the calibration offset; making this an accessor instead of a method leaves the caller unaware that a computation exists at all.
const measurementBehavior = {}; Object.defineProperty(measurementBehavior, "reading", { get() { return this.value + this.offset; }, set(newValue) { if (typeof newValue !== "number" || Number.isNaN(newValue)) { throw new TypeError("the reading must be a number"); } this.value = newValue - this.offset; }, enumerable: false, configurable: true, }); function measurementRecord(sensor, value, offset) { const record = Object.create(measurementBehavior); record.sensor = sensor; record.value = value; record.offset = offset; return record; } const record = measurementRecord("S-01", 21.4, -0.4); console.log(record.reading); record.reading = 25; console.log(record.value); console.log(record.reading); try { record.reading = "twenty"; } catch (error) { console.log(`${error.constructor.name}: ${error.message}`); } console.log(Object.hasOwn(record, "reading")); console.log(Object.hasOwn(measurementBehavior, "reading")); console.log(JSON.stringify(record));
21
25.4
25
TypeError: the reading must be a number
false
true
{"sensor":"S-01","value":25.4,"offset":-0.4}
The accessor is defined on the prototype; it is not the record’s own property. Even so,
the this inside the get and set bodies is bound to the object the access is made
on — that is, the record itself. A function found on the chain works with the data of the
object it is called on; the full rule for this will be covered in the first lesson of the
next topic.
The setter function is the direct way of preserving the class invariant introduced in the Introduction to Object-Oriented Programming lesson of the Programming Fundamentals course: an invalid value never enters the object at all. Also, because the accessor is defined as non-enumerable, it does not enter serialization; the last line shows only the raw data fields. This keeps computed values from mixing with stored data.
The get and set keywords in a class body are shorthand for this same definition; the
accessor they produce is also written to the prototype object, as non-enumerable.
Copying That Preserves Descriptors
The usual ways of copying an object do not carry descriptors. Object spread and
Object.assign read every property on the source and write it to the target as
an ordinary data property. Accessors turn into a frozen value in this operation.
const source = { sensor: "S-01", value: 21.4, get label() { return `${this.sensor}/${this.value}`; }, }; const spread = { ...source }; const definedCopy = Object.defineProperties( Object.create(Object.getPrototypeOf(source)), Object.getOwnPropertyDescriptors(source), ); console.log(source.label); console.log(spread.label); console.log(definedCopy.label); source.value = 30; spread.value = 30; definedCopy.value = 30; console.log(source.label); console.log(spread.label); console.log(definedCopy.label); console.log(typeof Object.getOwnPropertyDescriptor(spread, "label").get); console.log(typeof Object.getOwnPropertyDescriptor(definedCopy, "label").get);
S-01/21.4 S-01/21.4 S-01/21.4 S-01/30 S-01/21.4 S-01/30 undefined function
In the copy produced by spread, label is no longer computed; it is a fixed data
property carrying the text from the moment of copying, which is why it keeps showing the
old one once value changes. When Object.getOwnPropertyDescriptors is used together
with Object.defineProperties, both accessors and flags are preserved; the
Object.create call also carries the prototype link. Copying’s general problem — which
information is carried, which is dropped — will be taken up in full in the course’s last
lesson.
Summary
- Every property carries
writable,enumerable,configurableflags alongside its value;Object.getOwnPropertyDescriptorreads them. - Flags not specified in an
Object.definePropertycall are counted as closed; properties produced by assignment have all three open. - Assigning to a non-writable property throws an error in strict mode, silently fails in
non-strict mode;
configurable: falsecannot be reversed. - Non-enumerable properties do not enter
Object.keys,Object.entries,JSON.stringify, spread, andObject.assignresults; they are visible withObject.getOwnPropertyNames. - On an accessor property, reading and writing each run a function; the setter preserves the class invariant by keeping an invalid value from entering the object.
- Spread and
Object.assigndo not carry descriptors; they turn an accessor into a fixed value.
Next Step
writable: false locks a single property. Preventing an entire object from changing,
forbidding new properties from being added to it, or leaving only existing fields’
values changeable is tedious to write per property. The next lesson takes up the freezing
and sealing operations applied to a whole object, how deep they go, and the patterns for
producing a new object instead of a change when change is needed.
To keep your progress and take notes, Log in
My notes
Log in to take notes.