Class data is public and mutable

We've covered the core features of classes. You can create class instances, create instance properties with constructors, use methods, and extend parent classes.

However, there is a part of classes that you should be aware of. One of the major problems in JavaScript is that there are no private properties by default. For any objects all properties and methods are accessible by any one. Which means they can be mutated at any time.

Taking a look at our Product class, it has a property of price, which you initially set in the constructor. But we've changed the product class up a big. We have a method that can get the price (getPrice) and another that gets the clearance price, which by default is 50% off.

class Product {
  constructor(name, price, discountable) {
    this.name = name;
    this.price = price;
    this.discountable = discountable;
  }

  getClearancePrice() {
    return this.price * 0.5;
  }
}

const product1 = new Product("Coffee Maker", 99.95, false);

However, a user of the class can access the property on an instance by simply saying instance.price. Returning to our instance of SaleProduct, we can see this:

product1.price; // 99.95

And because an instance of Product is just an object, the price can be changed at will. But aside from being able to change the price to another number, our methods can be broken if a non-number is provided, say we provided an object. We can see this if we run .getClearancePrice():

product1.price = {};
product1.getClearancePrice(); // NaN

This poses a big problem: what can we do to with public data on objects to prevent data from being tampered with so that it breaks our program?

We can't make object data private (yet, more about that in the next lecture), but we can make use of two special classes features to help us: getters and setters.

How do we use getters and setters? Well we have a methods that are getting data: getClearancePrice(). This is clear because the function name is prefixed with get, in them we are retrieving data. As we know, to execute the method, you call it with dot syntax: instance.getClearancePrice().

But instead of having a method that we call, we can turn it to a getter, which will still have it function like before, as a method, but by using it like a property. To create a getter you add the keyword 'get' in front of the method. Then since get is repeated twice, we can remove it from the method name.

class Product {
  constructor(name, price, discountable) {
    this.name = name;
    this.price = price;
    this.discountable = discountable;
  }

  get clearancePrice() {
    return this.price * 0.5;
  }
}

const product1 = new Product("Coffee Maker", 99.95, false);

As I mentioned to use getters, you can use them like methods, but without the parentheses at the end, which gives it the appearance of a property:

product1.clearancePrice; // 49.975

This makes information easier to retrieve, but it doesn’t solve your problem of someone setting a value that is an invalid type. To fix that, we need to create a setter.

A setter works like the getter, but does the opposite. It is a method that works like a property, however a setter, accepts a single argument and changes a property rather than retrieves it. And since we use it like a property, like our getter, you don’t pass the argument using parentheses. Instead, you pass the value using the single equals operator (=) like you were actually setting a property.

To create a setter, you add the keyword set in front of a method. Inside the method, you can change a value on a property. Let's create a setter called newPrice, which will accept the price that we pass it and use it to update the price property:

class Product {
  constructor(name, price, discountable) {
    this.name = name;
    this.price = price;
    this.discountable = discountable;
  }

  get clearancePrice() {
    return this.price * 0.5;
  }

  set newPrice(price) {
    this.price = price;
  }
}

When it comes to setters, you need to have a corresponding getter, otherwise you can't retrieve the value that was set. We can see this if we try to use the newPrice setter:

product1.newPrice = 20;
product1.newPrice; // undefined

We have a setter but no getter, so we get undefined. That's why you should always try to pair getters and setters. Also they should have the same name. This is perfectly valid. With that being said, however, you can’t have an instance property available on this with the same name as your getter or setter.

In our case, for example, we can't name our getter or setter of the price property, price as well. It causes a very nasty error, which will likely freeze your application and cause an infinite loop.

What we can do is use another property to connect our getter and setter with the property. You don’t want users or other developers to access your bridge property. You want it to be for internal use only.

And the way that we indicate that to other developers, that a method or property should not be modified is by prefixing it with an underscore. So if you see a class with a property that begins with an underscore, know that it should be used outside the class itself or directly mutated.

After you set an intermediate property, you can use getters and setters with the same name, minus the underscore, to access or update the value.

Now we have all we need to fix the problem of the price (or any other property) being directly mutated. To create our bridge property, change the property this.price to this._price in the constructor, so it doesn't conflict with our getter and setter. After that, create a getter to access this._price and a setter that will reject any arguments passed to it that are not of the type number:

class Product {
  constructor(name, price, discountable) {
    this.name = name;
    this._price = price;
    this.discountable = discountable;
  }

  get price() {
    return this._price;
  }

  set price(price) {
    if (typeof price !== "number") {
      return this._price;
    } else {
      this._price = price;
    }
  }
}

And now you'll see that if we try to update the price by mutating it (or really using the price setter behind the scenes)and using an invalid type, we'll see that it doesn't work. We still get our original price returned to us. However, we still can change the price if it is a number if we need.

const product1 = new Product("Coffee Maker", 99.95, false);
product1; // Product {name: "Coffee Maker", _price: 99.95, discountable: false}
product2.price; // 99.95
product2.price = "alsdkjfas"; // "alsdkjfas"
product2.price; // 99.95
product2.price = 90; // 90
product1.price; // 90

Getters and setters can be a double edged sword. They are helpful to prevent mutations for breaking your application, but they can be confusing since they are still methods that operate as if they were properties.

Nothing in client-side JS is private

The final note is that in the end we ultimately realized something important in JavaScript, particularly when working with JavaScript on the client. None of our data is ultimately secure or private. If we truly need to keep our data private, for example, a user's password, it needs to not be available in our client-side JavaScript whatsoever.

Signaling intent with underscore

What we are doing with getters and setters and these pseudo private properties prefixed with underscores that is most important is that we see how to signal to other developers that certain properties are important and should not be used outside of the class.

As a final challenge to you, use the Product class that we have here and implement a getter and setter for the product's name property, where the setter rejects any value that is not a valid name.