Topic 6.7
Getters, Setters and Property Descriptors
In one line
A getter is a property that runs a function when you read it, and a setter runs a function when you assign to it; behind every property sits a descriptor that decides whether it can be changed, listed or deleted, which you read with Object.getOwnPropertyDescriptor and set with Object.defineProperty.
Think of it like this
A shop counter with a shopkeeper. When you ask "how much is the total?", the shopkeeper doesn't read a number off a board: they work it out right then from what's in your basket (a getter). When you say "make it 3 packets", they check the shelf before agreeing (a setter). And some things in the shop have rules on a little label: "display only, not for sale", "don't move" (property descriptors).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Data property
- A normal property that stores a value.
- Accessor property
- A property that runs a getter when read and/or a setter when assigned, instead of storing a value.
- Getter
- A function run when a property is read:
get area() { ... }. - Setter
- A function run when a property is assigned, receiving the new value:
set area(v) { ... }. - Derived value
- A value worked out from other values, like a total from prices.
- Property descriptor
- The hidden settings of a property:
valueorget/set, pluswritable,enumerableandconfigurable. - writable
- Whether a data property's value can be changed.
- enumerable
- Whether a property is listed by
Object.keys,for...in, spread and JSON. - configurable
- Whether a property can be deleted or have its settings changed.
- Object.defineProperty
- Creates or changes one property with exact descriptor settings; missing flags default to
false.
Step by step
01A getter for a derived value
The get keyword turns a method into a property. You read it without brackets, and it's recalculated each time, so it's never out of date.
const rect = {
width: 4,
height: 3,
get area() {
return this.width * this.height;
},
};
console.log(rect.area); // 12
rect.width = 10;
console.log(rect.area); // 3002A setter that checks the value
The set keyword runs code on assignment. Store the real value somewhere else, here _age, so the setter doesn't call itself.
const person = {
_age: 0,
get age() {
return this._age;
},
set age(value) {
if (value < 0) throw new RangeError("Age can't be negative");
this._age = value;
},
};
person.age = 14;
console.log(person.age); // 1403Look at the hidden settings
Every property has a descriptor. Normal properties have every flag set to true.
console.log(Object.getOwnPropertyDescriptor(rect, "width"));
// { value: 10, writable: true, enumerable: true, configurable: true }04Define a property with exact rules
Object.defineProperty sets the flags you choose. Everything you leave out becomes false, so this id can't be changed, listed or deleted.
const product = { name: "Pen" };
Object.defineProperty(product, "id", { value: 101 });
console.log(Object.keys(product)); // [ 'name' ]05Add a getter to an existing object
defineProperty can create accessors too. Pass get (and set) instead of value.
Object.defineProperty(product, "label", {
get() {
return this.name + " #" + this.id;
},
enumerable: true,
});
console.log(product.label); // Pen #101Try it yourself
- 1
Add a Kelvin getter
In the first example, add
get kelvin() { return this.celsius + 273.15; }. Predict what the lastconsole.log(temperature)shows now. - 2
Make the id enumerable
In the third example, change the descriptor to
{ value: 101, enumerable: true }. Predict the second line, then the result ofdelete. - 3
Remove strict mode
In the last example, delete the
"use strict";line. Predict the last two lines of the console.
Code & diagrams
Type in the box: the page re-reads the getter. Hectal's console shows a getter's current value; Chrome DevTools shows `fahrenheit: (...)` until you click it, and Node.js shows `[Getter/Setter]`.
Preview
Console
Nothing logged yet.Expected console output
77
100
{ celsius: 100, fahrenheit: 212 }The two bad assignments ran the setter, which refused them. Real code would usually `throw` an error instead of logging.
Preview
Console
Nothing logged yet.Expected console output
Rejected: -20
Rejected: lots
500Every flag left out of `defineProperty` became `false`: `id` is hidden from keys and JSON, the write was ignored and `delete` returned `false`.
Preview
Console
Nothing logged yet.Expected console output
101
[ 'name' ] {"name":"Pen"}
101
false 101
{ value: 'Pen', writable: true, enumerable: true, configurable: true }
{ value: 101, writable: false, enumerable: false, configurable: false }The descriptor shows `get` and `set` instead of `value` and `writable`. With no setter and `"use strict"` on, assigning throws, so the last line never runs.
Preview
Console
Nothing logged yet.Expected console output
12.57
28.27
{ get: [Function: get area], set: undefined, enumerable: true, configurable: true }
Uncaught TypeError: Cannot set property area of #<Object> which has only a getterBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A setter that sets itself
In the second example, change the setter's last line to this.balance = value;.
Break #2
Expect defineProperty to behave like assignment
Write Object.defineProperty(user, "email", { value: "a@example.com" }); and later user.email = "b@example.com";.
Myth vs fact
Myth
Getters are just methods, so call them with brackets.
Fact
A getter is read like a property: circle.area. Writing circle.area() reads the number and then tries to call it: TypeError: circle.area is not a function.
Myth
Spread or JSON.stringify copies the getter.
Fact
They call the getter and copy the value it returns at that moment. The copy has a plain data property. To copy accessors themselves, use Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj)).
Myth
Non-enumerable means private.
Fact
Hidden from listing only. Anyone who knows the name can still read it, and Object.getOwnPropertyNames lists it. Use #private fields (Phase 7.6) for real privacy.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Getters and setters in object literals are ES5 (2009);
Object.definePropertyis ES5 too. Vue 2's reactivity was built ondefinePropertygetters/setters; Vue 3 moved toProxy(Phase 14), which can also see added and deleted keys. - ▸
A lazy getter computes an expensive value on first read and then replaces itself with a data property: inside the getter,
Object.defineProperty(this, "data", { value: result }). Later reads skip the function entirely. - ▸
Setters on the prototype chain intercept assignment: if an inherited property is an accessor,
obj.x = 1calls the inherited setter instead of creating an own property; if it's a non-writable inherited data property, the assignment fails. This is how class setters (Phase 7.5) work. - ▸
Getters run on every read, including when DevTools or loggers inspect objects. Keep them fast and free of side effects; expensive work belongs in a method whose name signals the cost.
Remember this
- 1
So far every property has been a data property: it simply stores a value. An accessor property has no stored value; instead it has a getter function, run when the property is read, and/or a setter function, run when it's assigned. In an object literal you write them with
getandset:get fullName() { return this.first + " " + this.last; }andset fullName(text) { ... }. From outside they look exactly like normal properties:person.fullName, notperson.fullName(). - 2
Getters are good for derived values (worked out from other properties, so they're never out of date), like an area from a radius or Fahrenheit from Celsius. Setters are good for validation and conversion: reject a negative balance, trim a name, convert units. A setter receives exactly one argument, the assigned value. A property with only a getter is read-only: assigning to it is ignored in sloppy mode and throws a TypeError in strict mode.
- 3
A setter usually stores the real value in another property, often named with an underscore by convention, like
_balance. Never assign to the same property inside its own setter (set balance(v) { this.balance = v; }): that calls the setter again, forever, until the engine stops withRangeError: Maximum call stack size exceeded. The same happens with a getter that reads itself. (Classes in Phase 7.6 offer truly private#fieldsfor the stored value.) - 4
Every property has a hidden property descriptor, a small record of settings. A data property's descriptor has
valueand three flags: **writable(can the value change?),enumerable** (does it show up inObject.keys,for...in, spread andJSON.stringify?) and **configurable** (can it be deleted, or its flags changed?). An accessor's descriptor hasgetandsetinstead ofvalueandwritable. Read it withObject.getOwnPropertyDescriptor(obj, "key"), or all of them withObject.getOwnPropertyDescriptors(obj). - 5
Properties made the normal way (literal or assignment) have all three flags
true. **Object.defineProperty(obj, "key", descriptor)creates or changes a property with exact settings, and any flag you leave out defaults tofalse**. SoObject.defineProperty(product, "id", { value: 101 })makes anidthat is read-only, hidden from listing and can't be deleted. Built-ins use this all the time: that's whyObject.keys([1, 2])doesn't listlength, and why array methods don't show up infor...in. - 6
The locking tools from Topic 6.5 work by changing descriptors.
Object.preventExtensionsstops new properties.Object.sealalso makes every property non-configurable (no deleting).Object.freezealso makes every data property non-writable. Their checks areObject.isExtensible,Object.isSealedandObject.isFrozen. Getters and setters still run on a frozen object, because freezing makes accessors non-configurable but doesn't remove them.
Explain it without notes
What is the difference between a data property and an accessor property?
What are writable, enumerable and configurable?
Why does Object.defineProperty(obj, "x", { value: 1 }) behave differently from obj.x = 1?
What causes Maximum call stack size exceeded in a setter, and how do you avoid it?
Practice
Make person with first and last and an accessor fullName: reading gives "first last", assigning "Meera Nair" splits it into first and last. Log the result of both.
Use Object.defineProperty to add a read-only but listed createdAt to const note = { text: "Hi" }. Show that it appears in Object.keys but can't be changed.
Make cart with an items array of { price, qty } and a getter total. Log cart.total, push an item, log it again.
Trade-offs
- ↔
Getters keep derived values always correct and hide how they're computed, but they recompute on every read and look free to callers; cache or use a method when the work is expensive.
- ↔
Setters let you validate in one place without changing how callers write code, but hidden side effects on a simple-looking
obj.x = 1can surprise readers; keep setters small and predictable. - ↔
definePropertygives precise control (read-only, hidden), but itsfalsedefaults catch people out; use it for library code and constants, and plain assignment everywhere else.
Done when you can
I can write getters and setters in an object literal.
I can use a setter to validate and a getter for a derived value.
I can avoid the setter that calls itself.
I can read a property's descriptor and explain
writable,enumerableandconfigurable.I can create a read-only or hidden property with
Object.defineProperty.I know how freeze, seal and preventExtensions relate to descriptors.