Core JavaScript · 5 of 9
Classes make shared (and mutable state easy to default to — a method can read this.something instead of taking it as a parameter:
class Cart {
constructor(discount: number) {
this.discount = discount;
}
private discount: number;
addDiscount(items: Item[]) {
for (const item of items) {
item.price = item.price * (1 - this.discount);
}
return items;
}
}
const cart = new Cart(0.1);
cart.addDiscount(items);
addDiscount(items) doesn't show what discount it applies. You have to look at this.discount, and any method on the class is may have changed it.
Inheritance makes this harder - a subclass can override a method that changes this.discount, invisible at the call site.
class VipCart extends Cart {
applyLoyaltyBonus() {
this.discount += 0.05; // some unrelated method changes it
}
}
const cart = new VipCart(0.1);
cart.applyLoyaltyBonus();
cart.addDiscount(items); // which discount actually applies here?
Finding the answer means reading every method in the hierarchy that might touch this.discount. Testing it means building a Cart into the right state first
Instead call the function with and pass all values it depends on.
export const addDiscount = (items, discount) => {
const result = [];
for (const item of items) {
result.push({ ...item, price: item.price * (1 - discount) });
}
return result;
};
const discounted = addDiscount(items, 0.1);
// `items` is untouched; `discounted` is a new array
A function that only reads its inputs and returns a new value — a pure function — is safe to call from anywhere, since nothing it touches can surprise another caller. The loop above never touches items; it pushes onto a fresh result array and returns that. A for loop is fine — the rule is build new data and return it, not avoid loops.