Behavior
Problem
Everyone withdraws their own way
The bank has an Account class with an owner and a balance. Money is withdrawn in two places in the program. The ATM checks that there is enough money. The card payment was written by someone else, and it does not check:
const account = new Account("Maria", 100);
// The ATM checks that there is enough money.if (account.balance >= 70) { account.balance -= 70;}
// Someone else wrote the card payment. It has no check.account.balance -= 50;
console.log(account.balance);-20The account is below zero. The rule "you cannot withdraw more than you have" is important, but it is written (or forgotten) separately in every place that withdraws.
The idea
Methods live with the data
The withdrawal logic is part of the account, so put it in the Account class. A function written in the class is a method of the object:
class Account { owner; balance;
constructor(owner, balance) { this.owner = owner; this.balance = balance; }
deposit(amount) { this.balance += amount; }
withdraw(amount) { if (amount <= this.balance) { this.balance -= amount; } else { console.log(`Not enough money: ${this.owner}`); } }}
const account = new Account("Maria", 100);account.withdraw(70);account.withdraw(50);account.deposit(20);console.log(account.balance);Not enough money: Maria50- You write the method in the class without the word
function:withdraw(amount) { … }. - You call it on an object with a dot:
account.withdraw(70), just like the fields. - Inside,
this.balanceis the balance of this account, the object before the dot. Thethis.is required: without it,balanceis not the field. - The check is written once. Anyone who withdraws through the method goes through it.
const maria = new Account("Maria", 100);const peter = new Account("Peter", 40);maria.withdraw(70);peter.withdraw(70);console.log(maria.balance, peter.balance);Step by step, in words
- Two accounts, two objects. Each one has its own balance.
- maria.withdraw(70): this gets an arrow to the object before the dot.
- In the method, this.balance is Maria's balance: 100 - 70 = 30.
- Same method, different object: now this points to Peter.
- 70 is more than 40: the method prints a refusal, and Peter's balance stays the same.
- There is one copy of the method code. The fields belong to the object you call it on.
The card payment can no longer "forget" the check if it withdraws with withdraw. It can still write straight into the field, though. You will see how to stop that in the "Encapsulation" lesson.
The idea
Method or function
The functions from the "Functions" module live outside the class. They get everything as parameters and have no object of their own. A method works with the fields of the object it was called on:
function applyDiscount(price, percent) { return price * (100 - percent) / 100;}
const price = applyDiscount(10, 10);account.withdraw(70);You cannot call an instance method through the class name. It is not clear which account to withdraw from:
Account.withdraw(70);TypeError: Account.withdraw is not a functionThe withdraw method belongs to the objects, not to the Account class itself. Class methods exist too: they have static in front. You will see them in the lesson "static: what all objects share".
Try it
Whose account?
What will the program print?
const maria = new Account("Maria", 100);const peter = new Account("Peter", 40);maria.deposit(30);peter.withdraw(10);console.log(maria.balance, peter.balance);Each object has its own balance. maria.deposit(30) changes only Maria's balance, and peter.withdraw(10) changes only Peter's. Peter has 40, so withdrawing 10 goes through.
Try it
Find the bug
account.withdraw(30) stops the program with an error. Which line has the bug?Pick the line with the bug.
Without this., the name balance is an ordinary variable, and there is no such variable:
ReferenceError: balance is not definedThe field is this.balance. The next line writes it correctly, so the bug is only in the check.
The idea
A method can also answer
An instance method can return a result, just like the functions from the "Functions" module. A product knows its price and quantity, so it can tell you its own cost:
class Product { // the fields and the constructor, as before
total() { return this.price * this.quantity; }}
const cart = [ new Product("Bread", 1.20, 2), new Product("Milk", 2.10, 1), new Product("Coffee", 7.50, 1),];let sum = 0;for (const p of cart) { sum += p.total();}console.log(`Total: €${sum}`);Total: €12Compare this with the first lesson of the module: there, the sum was computed in the code below the class from the fields. Now that code only asks each product. If a bulk discount is added tomorrow, you change only total().
The idea
What does the object look like as text?
If you print a product directly, you get something useless:
const coffee = new Product("Coffee", 7.50, 1);console.log(`Purchase: ${coffee}`);console.log(coffee);Purchase: [object Object]Product { name: 'Coffee', price: 7.5, quantity: 1 }As text, the object is [object Object]. console.log of the object itself shows its fields, but that is for checking while you write code. It is not text you can put in a sentence.
The class can say how it should be shown. The method is called toString(), and it returns text:
class Product { // the fields and the constructor, as before
total() { return this.price * this.quantity; }
toString() { return `${this.name} x${this.quantity}: €${this.total()}`; }}
const coffee = new Product("Coffee", 7.50, 1);const bread = new Product("Bread", 1.20, 2);console.log(String(coffee));console.log(`In the cart: ${bread}`);Coffee x1: €7.5In the cart: Bread x2: €2.4You do not call it yourself. String(coffee), the template literal `In the cart: ${bread}` and "In the cart: " + bread call it for you when the object has to become text. Only console.log(coffee) does not call it: it still shows the fields.
The name must be exactly toString, with a lowercase t and an uppercase S. Where the object gets a toString before you even write one comes in the "Inheritance and polymorphism" module.
Try it
A book in a sentence
What will the program print?
class Book { title; author;
constructor(title, author) { this.title = title; this.author = author; }
toString() { return `${this.title} by ${this.author}`; }}
const book = new Book("Tobacco", "Dimitar Dimov");console.log("Reading: " + book);"Reading: " + book has to turn book into text. So the book's toString() is called, and it returns the title and the author.Exercise
Objects that do something
Solve the tasks in your editor. Take the classes from the earlier lessons and add methods to them.
Person: a methodintroduce()that prints the name and the age.SimpleCar: a methodaccelerate()that increases the speed by 10. Now the code below the class does not touch the speed directly.DanceMove: a methodperform()that "performs" the move.Book: a methodread()that prints "Reading [title] by [author]".
Create one object of each class and call its methods:
Hi, I'm Maria and I'm 34 years old.Speed after accelerating: 10Performing moonwalk for 5 secondsReading Under the Yoke by Ivan VazovExercise
A car with fuel
A Car class with a brand, a speed, fuel (in liters) and a mileage in kilometers. The constructor gets the brand and the fuel. The speed and the mileage start at 0.
accelerate()increases the speed by 10.refuel(liters)adds fuel, but the tank holds at most 50 liters.drive(distance)uses 6 liters per 100 km and adds the distance to the mileage. If there is not enough fuel, it prints a message and the car does not move.toString()returns the brand, the mileage and the fuel.
const car = new Car("Toyota", 20);car.accelerate();car.accelerate();console.log(`Speed: ${car.speed}`);car.drive(100);console.log(String(car));car.drive(300);car.refuel(40);console.log(String(car));car.drive(300);console.log(String(car));Speed: 20Toyota: 100 km, 14 LNot enough fuel for 300 kmToyota: 100 km, 50 LToyota: 400 km, 32 Ldistance * 6 / 100. In JavaScript, / always gives a decimal number, so for 30 km you get 1.8 liters.More tasks
- Discount. Add a method
applyDiscount(percent)toProductthat lowers the price of the product. Compare it with the function from the "Functions" module that returned a new price. - An account with a history. Add an operation counter to
Account: every deposit and every successful withdrawal increases it by 1. A methodtoString()returns the owner, the balance and the number of operations.
Test