Private Fields, Getters and Setters

A private field keeps an object's data out of reach of other code, a getter lets that code read it, a setter checks a new value before storing it, and static members belong to the class itself.

Journeyman · JavaScript · OOP · Updated

After this lesson you can

  • Keep an object's data in a private field and read it through a getter
  • Write a setter that checks a value before it is stored
  • Use a static method or property that belongs to the class, not to each object

Key takeaways

  • A private field's name starts with a hash sign, as in this.#shields. Only code inside the class can read or change it.
  • A getter, get shields() { ... }, is read like a property, with no parentheses. With no setter beside it, code outside can read the value but not change it.
  • A setter, set fuel(amount) { ... }, runs when code assigns a value, so it can check the value and refuse a bad one.
  • A getter can also work a value out from others, such as canFly from the shields and the fuel.
  • A static member belongs to the class itself, not to each instance. You read it on the class, as in Ship.MAX_FUEL.

In a game, a player who can type comet.shields = 99999 in the browser's console has made themselves unbeatable. In a banking app, code that sets an account's balance to -500 by mistake is far worse. Real classes protect their data: some values can only be read, and new values get checked before they're stored. You'll use this in every class that holds something that matters, and in this course's Virtual Pet project, whose hunger, energy and happiness can't go out of range.

The problem: anyone can change anything

The Ship class from Classes: Blueprints for Objects stores its shields as an ordinary property, so any code anywhere can set them:

class Ship {
  constructor(name, shields) {
    this.name = name;
    this.shields = shields;
  }
}

const comet = new Ship("Comet", 100);
comet.shields = 99999;
console.log(comet.shields);
99999

Nothing stops it. The ship's takeHit method can be as careful as you like; code that skips it and writes the property directly undoes all that care.

Private fields, written with `#`

Think of a bank. You can see your balance in the app, but you can't type in a new one: only the bank's own systems change it, by the rules (a deposit, a withdrawal).

A class can keep data private the same way. A field whose name starts with # (read "hash") is a private field: only code inside the class can see it. You declare it at the top of the class, then use it as this.#shields:

class Ship {
  #shields;

  constructor(name, shields) {
    this.name = name;
    this.#shields = shields;
  }

  takeHit(damage) {
    this.#shields = Math.max(0, this.#shields - damage);
    return this.#shields;
  }
}

const comet = new Ship("Comet", 100);
console.log(comet.takeHit(30));
console.log(comet.shields);
console.log(comet);
70
undefined
Ship { name: 'Comet' }

takeHit can use #shields, because it's inside the class. Outside, comet.shields is undefined (that's a different, missing property), and Node's printout doesn't even show the private field. Writing comet.#shields outside the class doesn't run at all: JavaScript refuses the whole file with SyntaxError: Private field '#shields' must be declared in an enclosing class.

Keeping an object's data behind its own methods like this has a name: encapsulation. In your textbook it's one of the main ideas of object-oriented programming.

Getters: read it, but don't touch it

Now Comet's shields are safe, but the game's screen can't show them. A getter lets other code read a value, while the class decides what it gets. You write get, a name and a body that returns the value. Other code reads it like a property, with no parentheses:

class Ship {
  #shields;

  constructor(name, shields) {
    this.name = name;
    this.#shields = shields;
  }

  get shields() {
    return this.#shields;
  }

  takeHit(damage) {
    this.#shields = Math.max(0, this.#shields - damage);
  }
}

const comet = new Ship("Comet", 100);
comet.takeHit(30);
console.log(comet.shields);

comet.shields = 99999;
console.log(comet.shields);
70
70

comet.shields runs the getter and reads 70. And comet.shields = 99999 changes nothing, because there's a getter and no setter. In a plain .js file Node ignores the line without a word; inside a class or a module file it stops with TypeError: Cannot set property shields of #<Ship> which has only a getter. Either way, the shields only change through takeHit.

Try it. Make shields.js with the class above. Add a line that tries console.log(comet.#shields); at the end and run it: read the error, then delete that line.

Setters: check a value before it's stored

Sometimes other code should be able to change a value, as long as it's a sensible one. Fuel can be topped up, but a tank holds 0 to 100 units, never -20.

A setter runs when code assigns a value. You write set, a name and one parameter, the new value. The setter decides whether to store it:

class Ship {
  #fuel = 0;

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

  get fuel() {
    return this.#fuel;
  }

  set fuel(amount) {
    if (amount < 0 || amount > 100) {
      console.log(`${this.name}: fuel must be from 0 to 100, not ${amount}.`);
      return;
    }
    this.#fuel = amount;
  }
}

const vega = new Ship("Vega");
vega.fuel = 80;
console.log(vega.fuel);

vega.fuel = -20;
console.log(vega.fuel);
80
Vega: fuel must be from 0 to 100, not -20.
80

vega.fuel = 80 looks like an ordinary assignment, but it runs the setter, which stores 80. vega.fuel = -20 runs the setter too, and the setter refuses: the fuel stays at 80. A getter and a setter usually come as a pair with the same name, around a private field. #fuel = 0 also shows that a private field can get a starting value where it's declared.

Here the setter refuses by printing a message and leaving the value alone. In Throwing Your Own Errors, in the next chapter, you'll learn a stronger way to refuse: stopping the program with an error that says what went wrong.

A getter can work a value out

A getter doesn't have to return a stored field. It can work out an answer from other fields every time it's read. A ship can fly while it has shields and fuel left:

class Ship {
  #shields;
  #fuel;

  constructor(name, shields, fuel) {
    this.name = name;
    this.#shields = shields;
    this.#fuel = fuel;
  }

  get canFly() {
    return this.#shields > 0 && this.#fuel > 0;
  }
}

const comet = new Ship("Comet", 70, 40);
const orion = new Ship("Orion", 0, 90);

console.log(comet.canFly);
console.log(orion.canFly);
true
false

canFly is never stored, so it can never be out of date: it's worked out fresh each time.

static: belongs to the class

Some values aren't about any one ship. The size of a fuel tank is the same for the whole fleet; a count of how many ships were built is about the class, not about Comet.

A member marked static belongs to the class itself. You read it on the class name, Ship.MAX_FUEL, not on an instance. A static method is a function you call on the class the same way:

class Ship {
  static MAX_FUEL = 100;
  static count = 0;

  constructor(name) {
    this.name = name;
    Ship.count = Ship.count + 1;
  }

  static fullTank() {
    return `A full tank is ${Ship.MAX_FUEL} units.`;
  }
}

const comet = new Ship("Comet");
const vega = new Ship("Vega");

console.log(Ship.count);
console.log(Ship.fullTank());
console.log(comet.count);
2
A full tank is 100 units.
undefined

There's one Ship.count for the whole class, and each constructor adds 1 to it. comet.count is undefined, because the count isn't on the instances. You've used static members before: Math.max and Object.keys are called on Math and Object themselves, never on an instance.

Try it. In shields.js, add static MAX_SHIELDS = 150; to the class, and change takeHit so a negative damage (a repair) can never lift the shields above Ship.MAX_SHIELDS.

Check yourself

2 questions to try on the lesson page.

So what

Private fields, getters and setters are how a class keeps its promises: shields only drop through takeHit, fuel is never negative, and nobody types in 99999. The more code (and the more people) use your class, the more those promises are worth.

Key terms

Cheat sheets

Related