Blog

JavaScript Property Descriptors, Getters and Setters

Every property in JavaScript carries three flags along with its value: writable, enumerable and configurable. Assignment sets all three to true, which is why they are easy to forget.

Object.defineProperty lets you set them yourself, and it defaults every one of them to false.

Look at a normal property

Object.getOwnPropertyDescriptor shows what a plain assignment produced.

// what a normal property looks like
const obj = { name: 'ada' };
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));

It prints:

{ value: 'ada', writable: true, enumerable: true, configurable: true }

Writable, listed by loops, and removable. The usual case.

defineProperty defaults to false

Defining a property without stating the flags gives you the opposite of an assignment.

// defineProperty defaults everything to false
const locked = {};
Object.defineProperty(locked, 'id', { value: 42 });
console.log(Object.getOwnPropertyDescriptor(locked, 'id'));
console.log('keys ->', Object.keys(locked), '| JSON ->', JSON.stringify(locked));
try { locked.id = 99; }                       // modules are strict, so this throws
catch (err) { console.log(err.constructor.name + ':', err.message); }
console.log('after write ->', locked.id);

It prints:

{ value: 42, writable: false, enumerable: false, configurable: false }
keys -> [] | JSON -> {}
TypeError: Cannot assign to read only property 'id' of object '#<Object>'
after write -> 42

The property is not writable, not listed by Object.keys, not included in JSON.stringify, and cannot be deleted or redefined. It is still readable by name.

A getter is a computed property

A getter runs a function when the property is read, so the value is always current.

// a computed property with a getter
const rect = {
  width: 3,
  height: 4,
  get area() { return this.width * this.height; }
};
console.log('area ->', rect.area);
rect.width = 10;
console.log('area ->', rect.area);
console.log('descriptor ->', Object.getOwnPropertyDescriptor(rect, 'area'));

It prints:

area -> 12
area -> 40
descriptor -> {
  get: [Function: get area],
  set: undefined,
  enumerable: true,
  configurable: true
}

The descriptor has get and set instead of value and writable. A property is either a data property or an accessor property.

A setter can refuse

The setter is the place to validate, because it runs on ordinary assignment.

// a setter that validates
const account = {
  _balance: 0,
  get balance() { return this._balance; },
  set balance(value) {
    if (typeof value !== 'number') throw new TypeError('balance must be a number');
    this._balance = value;
  }
};
account.balance = 100;
console.log('balance ->', account.balance);
try { account.balance = 'lots'; }
catch (err) { console.log(err.constructor.name + ':', err.message); }

It prints:

balance -> 100
TypeError: balance must be a number

Callers use it like a normal field and still cannot store nonsense in it.

Hiding and locking

enumerable: false keeps a property out of loops and JSON output. writable: false makes it read only.

// hide a property from loops and JSON
const record = { id: 1, name: 'ada' };
Object.defineProperty(record, 'secret', { value: 'hidden', enumerable: false });
console.log('keys ->', Object.keys(record));
console.log('JSON ->', JSON.stringify(record));
console.log('direct read ->', record.secret);

// a read-only property
const settings = {};
Object.defineProperty(settings, 'version', { value: '1.0', writable: false, enumerable: true });
console.log('version ->', settings.version);
console.log('descriptor ->', Object.getOwnPropertyDescriptor(settings, 'version'));

It prints:

keys -> [ 'id', 'name' ]
JSON -> {"id":1,"name":"ada"}
direct read -> hidden
version -> 1.0
descriptor -> {
  value: '1.0',
  writable: false,
  enumerable: true,
  configurable: false
}

This is how built in methods stay off Object.keys. Everything on Array.prototype is non enumerable, which is why for...in over an array does not list map and filter.

What to remember

  • Assignment creates properties with all three flags true.
  • Object.defineProperty defaults all three to false.
  • Getters and setters are accessor properties and have no value.
  • enumerable: false hides a property from loops and JSON.stringify, not from direct reads.

Descriptors are also what Object.freeze changes under the hood. Freezing sets writable and configurable to false on every own property.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply