Command Palette

Search for a command to run...

Hectal
PHASE 6Beginner ~26 min· topic 7 of 7

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: value or get/set, plus writable, enumerable and configurable.
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.

app.jswhole filejavascript
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); // 30

02A 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.

app.jswhole filejavascript
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); // 14

03Look at the hidden settings

Every property has a descriptor. Normal properties have every flag set to true.

app.jswhole filejavascript
console.log(Object.getOwnPropertyDescriptor(rect, "width"));
// { value: 10, writable: true, enumerable: true, configurable: true }
Look at the hidden settingsdiagram
Rendering diagram…

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.

app.jswhole filejavascript
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.

app.jswhole filejavascript
Object.defineProperty(product, "label", {
  get() {
    return this.name + " #" + this.id;
  },
  enumerable: true,
});
console.log(product.label); // Pen #101

Try it yourself

  1. 1

    Add a Kelvin getter

    In the first example, add get kelvin() { return this.celsius + 273.15; }. Predict what the last console.log(temperature) shows now.

  2. 2

    Make the id enumerable

    In the third example, change the descriptor to { value: 101, enumerable: true }. Predict the second line, then the result of delete.

  3. 3

    Remove strict mode

    In the last example, delete the "use strict"; line. Predict the last two lines of the console.

Code & diagrams

A temperature with a getter and setter New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

77
100
{ celsius: 100, fahrenheit: 212 }
A setter that validates New tab

The two bad assignments ran the setter, which refused them. Real code would usually `throw` an error instead of logging.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Rejected: -20
Rejected: lots
500
Reading and defining descriptors New tab

Every flag left out of `defineProperty` became `false`: `id` is hidden from keys and JSON, the write was ignored and `delete` returned `false`.

Preview

Press Run to see the page.

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 }
A getter-only property in strict mode New tab

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

Press Run to see the page.

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 getter

Break 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;.

terminal
$ Console
── what you'll see ──
Uncaught RangeError: Maximum call stack size exceeded

Break #2

Expect defineProperty to behave like assignment

Write Object.defineProperty(user, "email", { value: "a@example.com" }); and later user.email = "b@example.com";.

terminal
$ Console
── what you'll see ──
user.email is still 'a@example.com', and the key is missing from Object.keys(user) and JSON.

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.defineProperty is ES5 too. Vue 2's reactivity was built on defineProperty getters/setters; Vue 3 moved to Proxy (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 = 1 calls 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. 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 get and set: get fullName() { return this.first + " " + this.last; } and set fullName(text) { ... }. From outside they look exactly like normal properties: person.fullName, not person.fullName().

  2. 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. 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 with RangeError: Maximum call stack size exceeded. The same happens with a getter that reads itself. (Classes in Phase 7.6 offer truly private #fields for the stored value.)

  4. 4

    Every property has a hidden property descriptor, a small record of settings. A data property's descriptor has value and three flags: **writable (can the value change?), enumerable** (does it show up in Object.keys, for...in, spread and JSON.stringify?) and **configurable** (can it be deleted, or its flags changed?). An accessor's descriptor has get and set instead of value and writable. Read it with Object.getOwnPropertyDescriptor(obj, "key"), or all of them with Object.getOwnPropertyDescriptors(obj).

  5. 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 to false**. So Object.defineProperty(product, "id", { value: 101 }) makes an id that is read-only, hidden from listing and can't be deleted. Built-ins use this all the time: that's why Object.keys([1, 2]) doesn't list length, and why array methods don't show up in for...in.

  6. 6

    The locking tools from Topic 6.5 work by changing descriptors. Object.preventExtensions stops new properties. Object.seal also makes every property non-configurable (no deleting). Object.freeze also makes every data property non-writable. Their checks are Object.isExtensible, Object.isSealed and Object.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

01

What is the difference between a data property and an accessor property?

02

What are writable, enumerable and configurable?

03

Why does Object.defineProperty(obj, "x", { value: 1 }) behave differently from obj.x = 1?

04

What causes Maximum call stack size exceeded in a setter, and how do you avoid it?

Practice

01

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.

02

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.

03

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 = 1 can surprise readers; keep setters small and predictable.

  • ↔

    defineProperty gives precise control (read-only, hidden), but its false defaults 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, enumerable and configurable.

  • I can create a read-only or hidden property with Object.defineProperty.

  • I know how freeze, seal and preventExtensions relate to descriptors.