Skip to content
wannacode

Encapsulation

Problem

Minus €500

In the last lesson we put the withdrawal check in a method. But the field is open, and anyone can write to it directly:

const account = new Account("Maria", 100);
account.withdraw(500);
account.balance = -500;
console.log(account.balance);
Output
-500

The method correctly refused to withdraw €500. The next line simply set the balance by hand. The check is there, but you can get around it. We need a way to say "only the account touches the balance".

The idea

#balance: only the class touches it

Put # in front of the field name: #balance. The field must be declared in the class, and inside you write this.#balance:

class Account {
#owner;
#balance;
constructor(owner, balance) {
this.#owner = owner;
this.#balance = balance;
}
// ...
}

A field with # is private: only the code inside the Account class can reach it. If there is a line outside like

account.#balance = -500;

the program doesn't even start:

Error
SyntaxError: Private field '#balance' must be declared in an enclosing class

The class hides its data and shows it only through its own methods. This is called encapsulation. Java and C# have keywords for it, access modifiers (private, public). In JavaScript, private is #, and everything without # is public.

More precisely, if you are curious
  • Methods can be private too: #check() { … } is called only from the class, with this.#check().
  • Watch the name. account.balance = -500 without # doesn't touch the private field: it creates a new, public balance field on the object, with no error.
  • Before # existed (until 2022), JavaScript used the _balance convention, like Python. You'll see it in older code.

The idea

Reading without touching

From outside, we can't even read the balance now. So the class gives a method that only returns it:

bank.js
class Account {
#owner;
#balance;
constructor(owner, balance) {
this.#owner = owner;
this.#balance = balance;
}
get owner() {
return this.#owner;
}
get balance() {
return this.#balance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
}
const account = new Account("Maria", 100);
account.withdraw(30);
account.deposit(50);
console.log(`${account.owner}: €${account.balance}`);
Output
Maria: €120

get balance() is a getter: a method that you read from outside like a field, account.balance, with no parentheses. It returns the value of the private #balance. The balance changes only through deposit and withdraw, and they do the checks.

There is no setter, so the line account.balance = -500; changes nothing: JavaScript ignores the write, with no error. In strict mode (the line "use strict"; at the very top of the file), the same line stops the program:

Error
TypeError: Cannot set property balance of #<Account> which has only a getter

withdraw now returns true or false: whether the withdrawal went through. This way, whoever withdraws knows if it worked.

const account = new Account("Maria", 100);
account.withdraw(500);
account.deposit(50);
console.log(account.balance);
Step by step, in words
  1. #balance is private: the glass means there is no way to the field from outside.
  2. The withdrawal goes through the check in the method: 500 is more than 100. Refused, and the balance stays as it is.
  3. The deposit is checked too: 50 is a positive amount. The method adds it.
  4. The balance getter hands out a copy of the balance. The #balance field itself stays hidden.

Try it

Refused

What will the program print?

const account = new Account("Maria", 100);
console.log(account.withdraw(150));
account.deposit(-20);
console.log(account.balance);
Answer

The idea

Setter: a change with a check

Sometimes code outside needs to change a field, but not to just any value. An animal's age can change, but it can't be negative:

class Animal {
#age = 0;
get age() {
return this.#age;
}
set age(value) {
if (value >= 0) {
this.#age = value;
}
}
}
const cat = new Animal();
cat.age = 4;
cat.age = -3;
console.log(cat.age);
Output
4

set age(value) is a setter: the method that is called on cat.age = -3. value is the value on the right of =, here -3. The check fails and the age stays 4. From outside, the write looks like a write to a plain field, but it goes through the check.

The idea

Which field gets a setter?

Not every field needs a setter, and some don't even need a getter. Think about what code outside should be able to do with the product:

  • the name and the price are set when the product is created and are only read after that: getter only;
  • the quantity in the cart changes, but it's at least 1: a getter and a setter with a check.
class Product {
#name;
#price;
#quantity = 1;
constructor(name, price) {
this.#name = name;
this.#price = price;
}
get name() {
return this.#name;
}
get price() {
return this.#price;
}
get quantity() {
return this.#quantity;
}
set quantity(value) {
if (value >= 1) {
this.#quantity = value;
}
}
}
const coffee = new Product("Coffee", 7.50);
coffee.quantity = 3;
coffee.quantity = 0;
console.log(`${coffee.name} x${coffee.quantity}`);
Output
Coffee x3

The rule is: start with # for everything and open only what you need. A setter that stores anything without a check brings back the problem from the first card.

Try it

What's missing?

The balance should be visible only inside the class. What is the field called?
class Account {
   = 0;
}

Try it

Find the bug

The field is #balance, and yet the balance becomes -500:

const account = new Account();
account.balance = 100;
account.withdraw(30);
account.balance = -500;
console.log(account.balance);
Output
-500

Which line in the class breaks the protection?

Pick the line with the bug.

Exercise

Hidden data

Solve the tasks in your editor. All fields are private, with #.

1. Person. Person with a name and an age, a constructor, the getters name and age and introduce().

Output
Maria is 34 years old.
Hi, I'm Maria and I'm 34 years old.

2. An account with no minus. Account with an owner and a balance. A deposit accepts only a positive amount. A withdrawal returns whether it went through and never lets the balance go below 0. Code outside can read the balance, but can't write it.

The program
const account = new Account("Peter", 50);
account.deposit(25);
account.deposit(-10);
console.log(account.withdraw(100));
console.log(account.withdraw(40));
console.log(`Balance: €${account.balance}`);
Output
false
true
Balance: €35

3. Animal. Animal with a species and an age. The species is set in the constructor and is only read after that. The age has a setter (set age) that refuses a negative number and prints a message. The constructor goes through the same check too: in the constructor, write this.age = age;, without #.

The program
const cat = new Animal("cat", 3);
cat.age = 4;
cat.age = -1;
console.log(`${cat.species}, ${cat.age} years old`);
Output
Invalid age: -1
cat, 4 years old

More tasks

  • Grade. A Grade class with a subject name and a grade from 2 to 6. The setter refuses anything outside those limits. Try it with 5, 7 and 1.
  • Product with a discount. Add a method to the Product from the lesson that applies a discount in percent and accepts only 1 to 90%. The price has no setter.

Test

Check yourself

1. The field is #balance. Where can it be changed from?
2. Why does the account have no setter for the balance?
3. In set age(value), what is value?
4. What does encapsulation mean?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·