Getters, Setters & Property Descriptors

Med

JavaScript properties are more than simple key-value pairs. Behind every property is a descriptor that controls whether it can be written, enumerated, or deleted. Getters and setters let you intercept property access and assignment, enabling computed properties, validation, and lazy evaluation.

Interactive Visualization

Object Patterns

Old Way
const user = { first: "Alice", last: "Smith" };
const full = user.first + " " + user.last;
// Must compute manually every time
Modern Way
Click transform to see

Key Points

  • get/set syntax creates accessor properties that run code on read/write
  • Object.defineProperty controls writable, enumerable, and configurable flags
  • Data descriptors have value/writable; accessor descriptors have get/set — mutually exclusive
  • Object.getOwnPropertyDescriptor reveals the full descriptor of any property
  • Setters enable validation — reject invalid assignments before they happen
  • Getters enable lazy evaluation — compute values only when accessed

Code Examples

Getters and Setters in Objects

const user = {
  firstName: 'Alice',
  lastName: 'Smith',

  get fullName() {
    return `${this.firstName} ${this.lastName}`
  },
  set fullName(value) {
    const parts = value.split(' ')
    this.firstName = parts[0]
    this.lastName = parts[1]
  },
}

console.log(user.fullName) // 'Alice Smith'
user.fullName = 'Bob Jones'
console.log(user.firstName) // 'Bob'

Getters and setters look like regular property access but execute functions behind the scenes.

Validation with Setters

class Temperature {
  #celsius = 0

  get celsius() { return this.#celsius }
  set celsius(value) {
    if (typeof value !== 'number') throw new TypeError('Must be a number')
    if (value < -273.15) throw new RangeError('Below absolute zero!')
    this.#celsius = value
  }

  get fahrenheit() { return this.#celsius * 9 / 5 + 32 }
  set fahrenheit(value) { this.celsius = (value - 32) * 5 / 9 }
}

const temp = new Temperature()
temp.celsius = 100
console.log(temp.fahrenheit) // 212

Setters enforce constraints at the boundary. Invalid data never enters the object.

Object.defineProperty

const config = {}

Object.defineProperty(config, 'apiUrl', {
  value: 'https://api.example.com',
  writable: false,
  enumerable: true,
  configurable: false,
})

config.apiUrl = 'hacked' // silently fails
console.log(config.apiUrl) // 'https://api.example.com'

const desc = Object.getOwnPropertyDescriptor(config, 'apiUrl')
console.log(desc.writable) // false

Object.defineProperty gives fine-grained control over property behavior.

Lazy Evaluation with Getters

const analytics = {
  rawData: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],

  get average() {
    console.log('Computing average...')
    const sum = this.rawData.reduce((a, b) => a + b, 0)
    Object.defineProperty(this, 'average', { value: sum / this.rawData.length })
    return sum / this.rawData.length
  },
}

console.log(analytics.average) // 'Computing...' → 5.5
console.log(analytics.average) // 5.5 (cached!)

A getter can replace itself with a cached data property on first access — lazy evaluation pattern.

Common Mistakes

  • Creating infinite loops by accessing this.prop inside its own getter — use a backing field
  • Forgetting that Object.defineProperty defaults are all false
  • Mixing data descriptors (value/writable) with accessor descriptors (get/set) — throws
  • Not using strict mode — silent failures on read-only writes

Interview Tips

  • Know the 6 descriptor flags: value, writable, enumerable, configurable, get, set
  • Explain the lazy getter pattern for expensive computations
  • Vue 2 reactivity used Object.defineProperty getters/setters
  • Object.freeze uses defineProperty under the hood to set writable: false

Related Concepts