---
title: 'Property Descriptors'
source: 'https://academia.sh/en/courses/javascript-object-model/property-descriptors'
course: 'Objects and Functions in JavaScript'
language: en
updated: '2026-08-23T07:01:00+00:00'
license: 'CC BY-SA 4.0'
---

# Property Descriptors

The writability, enumerability, and configurability flags a property carries alongside its value; accessor properties and copying that preserves descriptors.

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`.

```js
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.

```js
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.

```js
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.

```js
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.

```js
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`, `configurable` flags alongside its
  value; `Object.getOwnPropertyDescriptor` reads them.
- Flags not specified in an `Object.defineProperty` call 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: false` cannot be reversed.
- Non-enumerable properties do not enter `Object.keys`, `Object.entries`,
  `JSON.stringify`, spread, and `Object.assign` results; they are visible with
  `Object.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.assign` do 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.
