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.definePropertydefaults all three to false.- Getters and setters are accessor properties and have no
value. enumerable: falsehides a property from loops andJSON.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.