skip to content

Property Descriptors, Getters and Setters

Every property is either a data property with writable/enumerable/configurable flags or an accessor pair, and `Object.defineProperty` is how you control that. It explains read-only properties, computed values and why some properties never show up in a loop.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, what do the `get` and `set` keywords do when written inside an object literal or a class body, and how does calling code use the property they create?

level: juniorimportance: must knowfreq 70%

answer

  1. reads and writes run your code
  2. no parentheses at the call site
  3. setter takes exactly one parameter
  4. backing field prevents self-recursion
  5. class accessors live on the prototype

basics

~20 s

get and set define an accessor property: functions the engine runs when code reads or assigns the property with ordinary dot syntax. The getter takes no arguments and returns the value; the setter takes exactly one argument.

solid answer

~50 s

`get name() { ... }` and `set name(v) { ... }` create an **accessor property** rather than a stored value. Calling code writes `obj.name` and `obj.name = 'Ada'` exactly as if it were a plain field — no parentheses — and the engine invokes the getter or setter behind that syntax, with `this` bound to the object the property was accessed on. A getter must take zero parameters and return the value; a setter takes exactly one parameter (the assigned value) and its return value is discarded. Typical uses are derived values (`get area() { return this.w * this.h; }`) and validation on write. The classic mistake is writing `this.name = v` inside `set name`, which re-enters the same setter and blows the stack; store into a differently named backing property such as `_name` instead. Declared in a class body, accessors live on the prototype and are shared by all instances.

go deeper

for a junior

Be able to write a getter and a setter in an object literal and say plainly that calling code uses plain dot syntax with no parentheses. Know that the setter takes one parameter.

for a middle

Explain that these create an accessor property with get/set instead of value/writable, that class accessors sit on the prototype while literal ones are own properties, and why assigning to the same name inside a setter recurses.

for a senior

Show the judgment about when a computed property should be an accessor at all: getters must stay cheap and side-effect free because callers cannot see the work hidden behind field syntax, and a throwing getter breaks copying and serialisation.

for a principal

Own the API-evolution argument — accessors let a stored field become computed, validated or lazily loaded without touching callers, but they also make cost invisible at the call site. Decide where your codebase draws that line and make it a written convention.

## Two kinds of property Every own property of a JavaScript object is exactly one of two kinds. A **data property** holds a value directly. An **accessor property** holds no value at all — it holds up to two functions, a *getter* and a *setter*, and the engine calls them when code reads or writes that property name. The `get` and `set` keywords are the literal-and-class syntax for creating accessor properties. ## Reading through a getter ```js const rect = { width: 3, height: 4, get area() { return this.width * this.height; }, }; console.log(rect.area); // 12 ``` `rect.area` is not a function reference: the engine calls the getter and the expression evaluates to its return value. Writing `rect.area()` would try to *call the result* (`12()`) and throw a `TypeError`. Inside the getter, `this` is the object the property was read on, which is why derived values can reference sibling properties. A getter runs on **every** read. Nothing is cached, so an expensive getter inside a loop is an expensive loop. ## Writing through a setter ```js const user = { _name: '', get name() { return this._name; }, set name(value) { if (typeof value !== 'string') throw new TypeError('name must be a string'); this._name = value.trim(); }, }; user.name = ' Ada '; console.log(user.name); // 'Ada' ``` The setter receives the right-hand side as its single parameter. Its return value is ignored — an assignment expression still evaluates to the assigned value, not to whatever the setter returned. The backing property `_name` matters. If the setter body said `this.name = value`, that assignment would trigger the same setter again, recursing until the engine throws `RangeError: Maximum call stack size exceeded`. The underscore is only a convention; it is a genuinely different property key, which is what breaks the cycle. ## Half-defined accessors You may define only one half: - **Getter only.** Reading works; assigning is a no-op in sloppy mode and throws a `TypeError` in strict mode. Class bodies and ES modules are always strict, so it throws there. - **Setter only.** Assigning works; reading yields `undefined`, because the missing getter is treated as `undefined`. ## Accessors in classes ```js class Temperature { constructor(celsius) { this.celsius = celsius; } get fahrenheit() { return this.celsius * 9 / 5 + 32; } set fahrenheit(f) { this.celsius = (f - 32) * 5 / 9; } } const t = new Temperature(100); console.log(t.fahrenheit); // 212 t.fahrenheit = 32; console.log(t.celsius); // 0 ``` Accessors written in a class body are installed once on `Temperature.prototype`, not copied onto each instance, and each instance reaches them through the prototype chain. Accessors written in an object literal are own properties of that one object. ## How they look as a descriptor The two kinds are visible through `Object.getOwnPropertyDescriptor`: ```js Object.getOwnPropertyDescriptor(rect, 'width'); // { value: 3, writable: true, enumerable: true, configurable: true } Object.getOwnPropertyDescriptor(rect, 'area'); // { get: [Function: get area], set: undefined, enumerable: true, configurable: true } ``` There is no `value` and no `writable` on an accessor property — "can I write to it" is answered by whether a setter exists. Literal accessors come out enumerable and configurable; ones declared on a class prototype are non-enumerable but still configurable and replaceable. The same accessor can be created programmatically: ```js Object.defineProperty(rect, 'perimeter', { get() { return 2 * (this.width + this.height); }, enumerable: true, configurable: true, }); ``` ## What accessors buy and what they cost They buy a stable public surface: a property can start life as a stored field and later become computed, validated, logged, or lazily loaded without a single call site changing. They also let you present a read-only view by supplying a getter and no setter. The cost is that property syntax stops being a promise of cheapness. Code that reads `order.total` has no syntactic hint that a database-shaped computation is hiding there, and a getter that throws will make ordinary-looking reads throw — including reads performed by `JSON.stringify` or by copying an object. Keep getters cheap and free of observable side effects; put anything expensive or fallible behind an explicitly named method instead.

  • What happens if code assigns to a property that has a getter but no setter?
    In sloppy mode the assignment is silently discarded — the read afterwards still returns the getter's value, which makes the bug easy to miss. In strict mode, which includes all class bodies and ES modules, it throws a `TypeError`. Define an explicit setter that throws a clear message if you want a loud failure everywhere.
  • Are getter results cached between reads?
    No — the getter body runs on every single read, so an expensive computation repeats each time. If you want memoisation you have to write it: cache into a backing field on first call, or have the getter replace itself with a plain data property using `Object.defineProperty(this, 'key', { value: computed })` so later reads skip the function entirely.
  • How do you define an accessor whose property name is decided at runtime?
    Use a computed key in the literal or class body — `get [keyName]() { ... }` — where `keyName` is any expression producing a string or symbol. Alternatively call `Object.defineProperty(obj, keyName, { get() { ... }, enumerable: true, configurable: true })`, which is the only option when the object already exists.

saying these in an interview costs you the question

  • Says you invoke a getter by writing obj.prop()
  • Thinks a setter can declare two parameters
  • Writes this.name = v inside set name and expects it to work
  • Believes getter results are cached after the first read
  • Claims class accessors become own properties of each instance

context

open as a page

In JavaScript, what does `Object.getOwnPropertyDescriptor(obj, key)` return for a data property versus an accessor property, and which attributes can the two kinds never share?

level: middleimportance: must knowfreq 58%

basics

~10 s

A data property's descriptor has value, writable, enumerable and configurable. An accessor property's has get, set, enumerable and configurable. Every property is exactly one kind, so value/writable and get/set can never appear together.

open as a page

Why does a property created with `Object.defineProperty(obj, 'id', { value: 1 })` behave differently in JavaScript from the same property created with `obj.id = 1`?

level: middleimportance: should knowfreq 50%

basics

~10 s

Attributes omitted from a defineProperty descriptor default to false, so the property is read-only, non-enumerable and non-configurable. Plain assignment creates it with writable, enumerable and configurable all true.

open as a page

A JavaScript object exposes `fullName` through a getter. You copy the object with `{ ...user }` — what does the copy actually contain, and how do you copy it so the accessor survives?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Object spread reads each own enumerable property, so the getter runs once and its result is stored as a plain data property — the copy is a frozen snapshot. Use Object.getOwnPropertyDescriptors to copy descriptors instead.

open as a page

Once a JavaScript property has been defined with `configurable: false`, what about it can still be changed, and which attempts throw a `TypeError`?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Non-configurable is a one-way door: the property cannot be deleted, reconfigured, or switched between data and accessor. The only permitted changes are on a still-writable data property — its value, and turning writable from true to false.

open as a page