Where this sits
Last session covered constructors, private fields and static. This one finishes OOP with encapsulation and inheritance, and it is the last JavaScript-only session: TypeScript and then Playwright itself come next.
Today's code is in 21_OOPs_Ecapsulation/, files 169 to 175, pushed during the session.
Encapsulation is control, not hiding
The textbook definition is "hide the internals" or "wrap data and functions together", and the session was blunt that this explains nothing. The analogy used instead: your family is a class, your children are fields, and a stranger is outside code. The stranger does not get to reach a child directly. They go through you.
So the useful definition is not about hiding. It is about deciding who is allowed to read and who is allowed to write.
class ICICI {
#balance;
constructor(name, balance) {
this.#balance = balance;
this.name = name; // public, no hash
}
getBalance() { return this.#balance; }
setBalance(balance, isCashier) {
if (isCashier) {
this.#balance = balance;
} else {
console.log("Not allowed");
}
}
}
Run on Node 22, exactly as the file does it:
| Call | Result |
|---|---|
pramod.getBalance() |
1000 |
pramod.setBalance(10000000, false) |
prints Not allowed, balance unchanged |
pramod.getBalance() |
still 1000 |
pramod.setBalance(300000, true) |
balance becomes 300000 |
pramod.#balance from outside |
SyntaxError: Private field '#balance' must be declared in an enclosing class |
Three rules, in the order they were given. Encapsulation works inside a class. Keep the variables private. Reach them only through getters and setters. And a fourth worth adding: getters and setters are for private fields. Writing them for a field that is already public is ceremony with no effect, which is why name in the example has none.
The real setter is not always the one called set
The second example is the one to take to an interview.
class TestCase {
#status = "not run";
static #count = 0;
constructor(name) {
this.name = name;
TestCase.#count++;
}
run(pass) { this.#status = pass ? "PASSED" : "FAILED"; }
getStatus() { return this.#status; }
static getCount() { return TestCase.#count; }
}
There is no setStatus. You cannot set the status directly at all, and that is deliberate: run() is the real setter. A status is something a test earns by running, not something a caller announces. Encapsulation is what lets you say that in code rather than in a code review.
Three things verified by running it. tc.run(true) then tc.getStatus() gives "PASSED". After four objects, TestCase.getCount() gives 4, because the counter is on the class. And TestCase.#count from outside is a SyntaxError, because a static private field is still private.
The trap:
tc.getCount()throwsTypeError: tc.getCount is not a function.getCountis static, so it lives on the class and not on instances. Same rule as last session'spramod_fn, pointing the other way.
Inheritance, and the five shapes
The analogy for why a child can use a parent's methods: you are allowed to use your father's house.
What `super` reaches, and what it does not
class Animal {
constructor(name) { this.name = name; }
eat() { return this.name + " is eating"; }
foo() { return "Animal.foo"; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // calls the parent constructor
this.breed = breed;
}
bark() { return this.name + " barks, and " + super.foo(); } // calls the parent method
}
Verified on Node 22 with new Dog("Rex", "Labrador"):
| Expression | Result |
|---|---|
d.name |
"Rex", so super(name) reached the parent constructor |
d.eat() |
"Rex is eating", inherited method, using the field the parent set |
d.bark() |
"Rex barks, and Animal.foo", so super.foo() reaches the parent method |
super.name inside Dog |
undefined |
That last row was flagged in class as a quirk. Running it gives a cleaner reason.
super. looks up the prototype chain. Instance fields are not on it. this.name is an own property of the object, created when the constructor ran. super.name looks on Animal.prototype, where no name exists. Confirmed both halves: the object has an own name, and Animal.prototype does not have one. So super reaches methods and the parent constructor, and never a field assigned with this. The rule is not "as of now", it is structural.
Method overriding
If the child defines a method the parent already has, the child's wins when you call it on a child object.
class BaseTest { setup() { return "BaseTest.setup"; } }
class UITest extends BaseTest { setup() { return "UITest.setup"; } }
class APITest extends BaseTest { } // no override
new UITest().setup(); // "UITest.setup"
new APITest().setup(); // "BaseTest.setup"
Both verified. The phrase used in class was जिसकी लाठी उसकी भैंस, whoever holds the stick owns the buffalo: whichever class the object belongs to, that class's method wins. Remove the override and it falls back up the chain.
The shape you will write in Playwright
This is why the session ends here rather than on a dog.
Two more things the session settled:
- Stick to plain
getX()andsetX()methods. JavaScript also hasgetandsetkeyword accessors, and they work, but the class is standardising on ordinary methods because JavaScript offers several syntaxes for the same thing and consistency is worth more than cleverness here. - Private methods are a TypeScript topic. Private fields with
#are JavaScript today; theprivatekeyword on methods comes with the TypeScript conversion.
Tasks and announcements
- Homework: the 182-exercise set. Work through it rather than reading it.
- Finish the three free certifications this week and post them on LinkedIn: Claude 101, Claude Code 101, and AI Fluency, with the plain-English companion if the framework is not sticking.
- Coming next: multiple, multi-level and hybrid inheritance, then polymorphism, then TypeScript, then Playwright itself: locator strategies, page object model, API, cucumber and the AI factory.
- Code for today: 21_OOPs_Ecapsulation.
- Previous: constructors, private fields and static.