ESC

Type to search the knowledge base.

Getters and Setters

get/set accessors on objects and classes — computed properties, validation, infinite loop traps, and defineProperty.

beginner3 min read
  • javascript
  • getters
  • setters
  • objects

Getters and setters look like properties but run functions on read/write. Use them for derived values, validation, and lazy init — not for hiding heavy side effects behind innocent obj.foo access.

Object literal syntax

const user = {
  first: 'Ada',
  last: 'Lovelace',
  get fullName() {
    return `${this.first} ${this.last}`;
  },
  set fullName(value) {
    const [first, ...rest] = value.split(' ');
    this.first = first;
    this.last = rest.join(' ');
  },
};

user.fullName; // "Ada Lovelace"
user.fullName = 'Grace Hopper';
user.first; // "Grace"

Classes

class Celsius {
  #c = 0;

  constructor(c) {
    this.c = c;
  }

  get c() {
    return this.#c;
  }
  set c(n) {
    if (typeof n !== 'number' || Number.isNaN(n)) {
      throw new TypeError('expected number');
    }
    this.#c = n;
  }

  get f() {
    return this.#c * 1.8 + 32;
  }
  set f(v) {
    this.c = (v - 32) / 1.8;
  }
}

const t = new Celsius(0);
t.f; // 32
t.f = 212;
t.c; // 100

Infinite recursion trap

const bad = {
  get x() {
    return this.x; // stack overflow — calls itself
  },
  set x(v) {
    this.x = v; // same problem
  },
};

Store in a different field (_x, #x, or a WeakMap).

defineProperty

const obj = { _n: 0 };
Object.defineProperty(obj, 'n', {
  enumerable: true,
  configurable: true,
  get() {
    return this._n;
  },
  set(v) {
    this._n = Number(v) || 0;
  },
});

Accessors from defineProperty can be non-enumerable — useful for libraries.

When getters hurt

const report = {
  get rows() {
    return expensiveQuery(); // runs every read
  },
};
// console.log(report.rows, report.rows) → twice the cost

Readers assume property access is cheap. For expensive work, use a method getRows() or memoize explicitly.

get rows() {
  if (!this._rows) this._rows = expensiveQuery();
  return this._rows;
}

Invalidate _rows when inputs change.

Serialization

JSON.stringify({
  get x() {
    return 1;
  },
});
// '{"x":1}' — getter is invoked

// setters don't participate in stringify

Object.assign and spread invoke getters and copy values (not the accessor):

const copy = { ...user }; // fullName becomes a data property string

Interview answer

“Getters and setters are accessor properties that run code on get/set. I use them for derived fields and validation with a separate backing store to avoid recursion. I avoid heavy side effects in getters because they look like cheap fields. Spread and JSON.stringify invoke getters and snapshot values.”

Proxy vs accessors

Accessors are fixed per property. A Proxy traps all gets/sets dynamically (validation frameworks, reactive systems). Prefer explicit getters for known derived fields; reach for Proxy when the key set is open-ended.

const bag = {
  _values: new Map(),
  get(key) {
    return this._values.get(key); // method, not accessor
  },
};
// don't pretend dynamic keys are accessors without Proxy

In React, derived values are usually plain expressions in render or useMemo, not OOP getters on state objects — different style, same idea: compute from source fields without storing duplicates that go stale.

enumerable and tooling

Object.defineProperty(obj, 'total', {
  enumerable: true,
  get() {
    return this.price * this.qty;
  },
});

Non-enumerable accessors hide from Object.keys and spread — useful for internal caches, confusing if you expected them in logs. Debuggers show getters as properties; beware evaluating a throwing getter while hovering in DevTools.

Further reading

Related guides