Getters and Setters in Javascript: What's the POINT?
Tags: javascript, webdev, beginners, tutorial
Published: 2021-12-10 02:08:45

## The Why

Mentoring on FreeCodeCamp and The Odin Project, you'll often see the `Thermometer` project as an introduction to getters and setters in javascript. You know the one:

```js
class Thermostat{
  constructor(fahrenheit){
    this.fahrenheit = fahrenheit;
  }
  get temperature(){
    return 5/9 * (this.fahrenheit-32)
  }
  set temperature(tempInC){
    this.fahrenheit = tempInC * 9/5+32
  }
}

const thermos = new Thermostat(76); // Setting in Fahrenheit scale
console.log(thermos.temperature); // 24.44 in Celsius
thermos.temperature = 26;
console.log(thermos.temperature); // 26 in Celsius
```

And that's lovely. Does exactly what we want, defines a pretty *interface* for the `temperature` property on the `Thermostat` object. But it's *terrible*, in that not only is that `temperature` an exposed property, so is the `fahrenheit`. Given that the properties are public anyway, what's the point of getters and setters?

## More Why

We *could* sidestep the issue by using ES6's private properties, simply doing this:

```js
class Thermostat{
  constructor(fahrenheit){
    this.#fahrenheit = fahrenheit;
  }
  get temperature(){
    return 5/9 * (this.#fahrenheit-32)
  }
  set temperature(tempInC){
    this.#fahrenheit = tempInC * 9/5+32
  }
}
```
Now, from the outside, `Thermostat.fahrenheit` no longer exists. Its a private property. Thank you, ES6!

And yet, I am not a fan. Private properties or methods (and private static properties or methods) just feel like a hacky duct-tape solution to a problem that doesn't actually exist. Why? Because we already *had* private properties.

## The What

Private "sandboxes" for our data are nothing new. Javascript has always kept a private *scope* for functions. And if you've been at this a bit, you'll see reference to *closures*. A closure is composed of two separate parts:

1. A private scope, contained within a function, and
2. Some means of accessing variables within that scope.

You see, functions execute, create their private scope, set up their variables, do their instructions, then quietly get swept out with the trash. As soon as nothing is observing the variables in a function, its data becomes available for garbage collection, freeing that memory for other code.

But we don't *have* to allow that. By returning something that continues to observe that function's scope, *even after the function is done executing*, we can continue to maintain and update the values contained within it.

Let's take a look at that `Thermometer` example again, this time with a closure:

```js
const Thermostat = (fahrenheit) => {
  // here, we have the variable fahrenheit.
  //  completely hidden from the outside world.

  // we'll define those same getters and setters
  // but note we access the variable, not a property
  return {
    get temperature(){
      return 5/9 * (fahrenheit-32)
    },
    set temperature(tempInC){
      fahrenheit = tempInC * 9/5+32
    }
  }
}

// note this: we aren't using Thermometer as an
//  object constructor, simply as an executed function.
const thermos = Thermostat(76);

// and from here on, it works exactly the same!
console.log(thermos.temperature); // 24.44 in Celsius
thermos.temperature = 26;
console.log(thermos.temperature); // 26 in Celsius
```

So we *have* private data within that closure, in the variables. And we define an accessor object, and return that. That defines the **interface** we use to talk to that private data.

##The Gotcha

Again, when fielding questions on The Odin Project's Discord server, I'll field this same gotcha multiple times a week. It's a biggie, and it doesn't always make sense. Consider this:

```js
const TicTacToe = ()=>{
  let board = new Array(9).fill("");
  let player1 = {name: 'Margaret', icon: 'X'};
  let player2 = {name: 'Bert', icon: 'O'};
  let currentPlayer = player1;

  const switchPlayers = () => {
    if(currentPlayer===player1){
      currentPlayer=player2;
    } else {
      currentPlayer=player1;
    }
  }

  // and our return interface:
  return {
    switchPlayers,
    currentPlayer,
    board
  }
};

// let's make a board!
const game = TicTacToe();

// And let's play a little!
game.board[4] = game.currentPlayer.icon;
console.log(game.board);
// [null, null, null, null, 'X', null, null, null, null]

// switch to player2...
game.switchPlayers();
game.board[0] = game.currentPlayer.icon;
console.log(game.board)
// ['X', null, null, null, 'X', null, null, null, null]
```

Did you note that last return? `game.board[0]`, which we set to `game.currentPlayer.icon`, is the wrong player! Did our `game.switchPlayers()` not work?

Actually, it did. If you were to open the browser's dev tools and inspect the variables inside that closure, you'd see that `currentPlayer===player2`. **But** `game.currentPlayer` is still referring to `player1`.

This is because, when we created the object that we returned inside our closure, we referred to the variable as a static reference to the value at the moment we created it. We took a snapshot of that primitive. Then we update the variable, pointing it to a new memory location, but the object property is completely disconnected from the variable!

*"Yeah, but what about the `game.board`? We're updating that on the object and it's updating the variable, right?"*

You're absolutely right. We do `game.board[4]='X'`, and that is updating *both* the variable, and the returned object property. The reason? We're *mutating* that array. We're mucking about with its insides, but we are leaving the variable and property *reference* alone. Suppose we wanted to reset the board, we could do this:

```js
game.board = new Array(9).fill("");
```

Clears the `game.board`, all set for another! And what we've just done is the same problem in reverse. We've changed the thing that `game.board` refers to, pointed it at a new location in memory, *but the variable still refers to the original*.

Well, that isn't our intent at all!

## Once More With the Why

Why did that happen? Because we sort of abandoned one of the principle tenets of Object Oriented development. There are three:

* Encapsulation *(how can we hide our stuff?)*
* Communication *(how can we set and get our hidden stuff?)*
* Late Instantiation *(can we dynamically make new stuff as we execute?)

We have the third one down pat, but we've sort of trampled on the first two. By exposing our data directly on the returned object, it is no longer encapsulated, and our communcation is questionable.

## The How

The solution? We create an *interface* and return that! We want to be able to `switchPlayers`, and we want to be able to get the `currentPlayer`. We also want to see the *state* of the `board` at any point, but we should *never* set that directly. We might also want to be able to reset the board at some point.

So let's think about an interface:

* For the player, we likely want to be able to get their name and icon. That's pretty much it.
* For the board, it'd be nice to be able to get or set a value at a particular cell, reset the board, and get the value of the board as a whole.
* For the game, how about we expose that board (the interface, not the data), create that switchPlayers function, and make currentPlayer an interface method, rather than directly exposing the data?

That's pretty much it. We could add the `checkForWin` functionality to either the board or the game, but that isn't really relevant to this as an exercise in data encapsulation.

With that, let's code!

```js
const Player = (name, icon) => {
  return {
    get name(){ return name; },
    get icon(){ return icon; },
  }
}

const Board = () => {
  let board = new Array(9).fill("");
  // .at will be an interface method,
  //  letting us get and set a board member
  const at = (index) => ({
    get value(){ return board[index] },
    set value(val){ board[index] = val; }
  })
  const reset = () => board.fill("");

  return {
    at,
    reset,
    get value(){ return [...board];}
  }
}

const TicTacToe = (player1Name, player2Name)=>{
  let board = Board();
  let player1 = Player(player1Name, 'X');
  let player2 = Player(player2Name, 'O');
  let currentPlayer = player1;

  const switchPlayers = () => {
    if(currentPlayer===player1){
      currentPlayer=player2;
    } else {
      currentPlayer=player1;
    }
  }

  // and our return interface:
  return {
    switchPlayers,
    board,
    get currentPlayer(){ return currentPlayer; }
  }
};

// now we can:
const game = TicTacToe('Margaret','Bert');
game.board.at(4).value=game.currentPlayer.icon;
console.log(game.board.value);
// ['','','','','X','','','','']

// all good so far, but now:
game.switchPlayers();
game.board.at(0).value=game.currentPlayer.icon;
console.log(game.board.value);
// ['O','','','','X','','','','']
```
Nice! Now, because we are not working with the data **directly**, we can manipulate the data by a clean, consistent interface. If we work with the `board` interface methods, we consistently refer to the internal *state* data, rather than the exposed *reference* point.

Now, there is a serious gotcha to consider here. What might happen if we did this?

```js
game.board = new Array(9).fill('');
```

With that, we've again broken the connection between the internal `board` variable and the exposed `board` interface. We haven't solved **ANYTHING**!

Well, we *have*, but we're missing a step. We need to protect our data. So a small change to all of our factory methods:

```js
const Player = (name, icon) => {
  return Object.freeze({
    get name(){ return name; },
    get icon(){ return icon; },
  });
};

const Board = () => {
  // all the same code here...

  return Object.freeze({
    at,
    reset,
    get value(){ return [...board];}
  });
};

const TicTacToe = (player1Name, player2Name)=>{
  // all this stays the same...

  return Object.freeze({
    switchPlayers,
    board,
    get currentPlayer(){ return currentPlayer; }
  });
};
```

By applying `Object.freeze()` to each of those factories' returned objects, we prevent them from being overwritten or having methods added unexpectedly. An added benefit, our getter methods (like the `board.value`) are truly read-only.

## The Recap

So getters and setters in the context of a factory are very sensible to me, for a number of reasons. First, they're object methods that are interacting with truly private variables, making them *privileged*. Second, by defining just a getter, we can define read-only properties quickly and easily, again going back to a solid interface.

Two more less obvious points I really like about getters and setters:

* When we `Object.freeze()` our objects, **any primitive data on that object is immutable**. This is really useful, but our exposed setters? Yeah, they still work. They're a method, rather than a primitive.

* **BUT**, when we `typeof game.board.at`, we will be told that it's data of type `function`. When we `typeof game.board.at(0).value`, we will be told that it's data of type `string`. **Even though we know it's a function**!

This second point is wildly useful, and often unappreciated. Why? Because when we `JSON.stringify(game)`, all of its `function` elements will be removed. `JSON.stringify()` crawls an object, discards all functions, and then turns nested objects or arrays into strings. So, if we do this:

```js
json.stringify(game);
/****
 * we get this:
 *
 *{
 *  "board": {
 *    "value": [
 *      "O",
 *      "",
 *      "",
 *      "",
 *      "X",
 *      "",
 *      "",
 *      "",
 *      ""
 *    ]
 *  },
 *  "currentPlayer": {
 *    "name": "Bert",
 *    "icon": "O"
 *  }
 *}
 ****/
```

This seems silly, maybe - but what it means is, with well-defined getters, we can have a saveable state for our objects. From this, we could re-create most of the `game` later. We might want to add a `players` getter, giving us an array of the players themselves, but the point remains... getters and setters are more useful than we think at first glance!