OOP principles in JavaScript
Use encapsulation, abstraction, polymorphism, factories, classes, and practical SOLID rules to design JavaScript objects that are easy to change and test.
- 01Give objects a small public interfaceHide an object's data with private fields or closures, and offer a few methods that check every change.
- 02Use polymorphismWrite a loop that calls a method by name and works with any object that has that method, whatever its class.
- 03Choose a factory or a class, and use SOLIDCompare the two main ways to make objects, and use the five SOLID rules to make code easier to change and test.
What OOP means in JavaScript
Object-oriented programming, or OOP, is a way to organize code around objects. An object holds some data, and the methods (functions) that work on that data. You don't have to use OOP to write JavaScript. You can also build good programs with plain functions, modules, closures, and plain objects. Most real programs mix all of these.
The useful core of OOP is small. An object offers a short list of methods that other code may call. That list is the object's public interface. Other code uses only that list. Everything else stays hidden inside the object.
Here are two objects from this lesson:
- A
ThermostatofferssetTarget()anddisplay. - A
Circle, aSquare, and aTriangleall offerarea()anddraw().
Code that uses these objects calls only those methods. It doesn't need the hidden parts: private fields, maths formulas, or how the object stores its data.
In JavaScript, OOP means designing objects in three ways:
- Give each object a small public interface: a short list of methods.
- Hide the data that other code should not change directly.
- Let different objects answer the same method call, each in its own way.
A class is one tool for making such objects. Factory functions, closures, delegation, and mixins are tools too.
This lesson ends the Classes module. You have already seen:
- class syntax;
extendsandsuper;- static members;
#privatefields;- extending built-in classes;
- mixins.
Two earlier lessons help here too: Closure patterns and Delegation vs classical inheritance. They show that JavaScript doesn't have to copy the class style of languages like Java. Stage 9 goes further, with Creational patterns and Structuring an application. This lesson stays practical, with code you can run.
A busy kitchen gets messy when everyone works at every station. It gets messy when one tool tries to do every job. It gets messy when a new pan behaves in a surprising way. Good kitchens follow a few simple rules to stop this.
SOLID is a set of five rules like these, for code. Each letter is the first letter of one rule. You'll meet each rule later in this lesson. They are reminders that help when a program grows, not laws to follow every time.
- In real life: One cook looks after the sauce station
- In JavaScript: Single Responsibility: each piece of code has one job
- In real life: Add a new dish without rewriting every order ticket
- In JavaScript: Open/Closed: add new kinds of things without editing code that works
- In real life: A new pan must still work on the same stove
- In JavaScript: Liskov Substitution: a replacement object keeps the same promises
- In real life: Use small tools, not one giant gadget
- In JavaScript: Interface Segregation: ask only for the methods you use
- In real life: Plug in any mixer that fits the socket
- In JavaScript: Dependency Inversion: be given your tools instead of building them in
Encapsulation & abstraction
STEP THROUGHEncapsulation means keeping data, and the methods that look after it, together inside one object. The object protects its own rules. A rule here is something that must always be true, like "the target temperature is between 10 and 35".
JavaScript gives you several ways to hide data inside an object:
#privatefields and methods in a class. Only code inside the class body can use them.- Variables in a closure. A closure is a function that keeps using the variables from the place where it was made.
- Variables in a module that the module doesn't export.
- A naming habit, like starting a property name with
_. This only asks people not to touch it. It doesn't stop them.
Which way you choose matters less than the result: outside code gets fewer ways to break the object's rules.
Abstraction means giving other code a simple idea to use, and hiding the steps behind it. Other code should just write thermostat.setTarget(22). It should not have to check the range, store the number, and build the display text itself. The public method is the simple idea. The private field and the checks are the hidden steps.
class Wallet {
#balance = 0;
add(amount) {
this.#balance += amount;
return this.#balance;
}
}
const wallet = new Wallet();
console.log(wallet.add(3));
console.log(wallet.balance);Line 2 creates a private field, #balance, inside Wallet. Only code inside the class can use it. Line 5 changes it from inside the add() method, so line 11 prints 3. Line 12 asks for wallet.balance, with no #. That is a different, public property. The wallet has no such property, so line 12 prints undefined.
You drive a car with the steering wheel, the pedals, and the dashboard. You don't reach into the engine while you drive. A well-made class works the same way. It gives you controls for normal use. It keeps the parts that break easily behind a cover.
- In real life: The steering wheel and pedals
- In JavaScript: Public methods, like
setTarget() - In real life: The engine, belts, and fuel pipes
- In JavaScript: Private fields, like
#targetCelsius - In real life: The speedometer shows your speed
- In JavaScript: A getter, like
display, shows a value you can read but not change
A menu gives you simple names: pasta, soup, salad. It doesn't hand you the recipe for boiling water. You say what you want, and the kitchen does the steps.
- In real life: You order "pasta"
- In JavaScript: You call
thermostat.setTarget(22) - In real life: The kitchen boils water, adds salt, and cooks the sauce
- In JavaScript: The method's hidden steps: check the range, then store the number
- In real life: The menu lists only what you may order
- In JavaScript: The public interface lists only what you may call
function printArea(shape) {
return "Area: " + shape.area();
}
const tile = {
area() {
return 12;
},
color: "blue",
};
console.log(printArea(tile));printArea() uses only one method of the object it gets: area(). The tile object also has a color, but printArea() never looks at it. So line 12 prints Area: 12. Because printArea() asks for so little, any object with an area() method works with it.
Now try the thermostat below. It has one private field, #targetCelsius, which starts at 20. It has two public parts:
setTarget(value)asks for a new temperature. It accepts only 10 to 35°C.displayis a getter: a method you read like a property, with no brackets. It returns the temperature as text.
Pick 22, then 41 or -5, and step through. For 41 and -5, the method says no before it changes anything. The private field stays at 20, because setTarget() is the only way in, and it checks first.
Pick a temperature, then step through. The private field can only change through setTarget(), and that method checks first.
script
class Thermostat { #targetCelsius = 20; setTarget(value) { if (value < 10 || value > 35) { return "Refused " + value + "°C"; } this.#targetCelsius = value; return "Target is " + this.#targetCelsius + "°C"; } get display() { return this.#targetCelsius + "°C"; }} console.log(thermostat.display);console.log(thermostat.setTarget(22));console.log(thermostat.display);setTarget() is the only way to change #targetCelsius, and it checks the value first. 22 is between 10 and 35, so it is saved; 41 is not, so the field stays 20.The code you write today may use the object correctly. Code written later might not. Encapsulation keeps each rule in one place, so a new button, an import script, or a test helper can't skip it by mistake.
It also makes changes safer. If other code uses only display and setTarget(), the class can change how it stores the temperature later. None of that other code has to change.
Polymorphism: one method call, many answers
INTERACTIVEPolymorphism means "many forms". In JavaScript the idea is simple. Several objects can have a method with the same name. Code can call that method on any of them. Each object runs its own version.
JavaScript doesn't ask you to declare this anywhere. The code just calls the method, and the object decides what happens.
A universal remote has one power button. Point it at a TV, a speaker, or a lamp, and press it. Each device turns on in its own way. The button stays the same.
- In real life: One power button
- In JavaScript: One method call:
shape.area() - In real life: The TV, the speaker, and the lamp each turn on in their own way
- In JavaScript:
Circle,Square, andTriangleeach run their ownarea() - In real life: The remote doesn't need to know which device it points at
- In JavaScript: The loop doesn't check which class each shape is
The shapes example below also shows a SOLID rule called Open/Closed. It means you can add new kinds of things without editing the code that already works:
- The loop is closed to changes. It always calls
draw()andarea(). - The program is open to new shapes. Add a
Trianglewith the same two methods, and the loop works with it as it is.
Near the end of the code, the shapes array (a list) holds the shapes. The for...of loop below it visits each shape in turn and calls the same two methods. This is simpler than a chain of checks like if (shape.name === "circle"), which you would have to edit for every new shape.
Guess each printed line first. Then pick Add Triangle above, and see that the loop doesn't change.
script
class Circle { constructor(radius) { this.name = "circle"; this.radius = radius; } area() { return Math.PI * this.radius ** 2; } draw() { return `● circle radius ${this.radius}`; }} class Square { constructor(size) { this.name = "square"; this.size = size; } area() { return this.size * this.size; } draw() { return `■ square ${this.size}×${this.size}`; }} for (const shape of shapes) { console.log(shape.draw() + " has area " + shape.area().toFixed(2));}area(), so each gives a different answer.JavaScript code often uses duck typing. The name comes from a saying: "If it walks like a duck and quacks like a duck, treat it as a duck." In code it means: if an object has the method you need, use it. That's powerful, but it needs care:
- Give methods clear names.
- Keep each method small.
- Test that every object really has the shared methods and returns the right kind of value.
The shapes loop works only if every shape keeps its side of the deal.
Factories vs classes
COMPAREJavaScript has more than one way to make objects. These are the two most common:
- A class is a blueprint. You make objects from it with
new, likenew Counter("class"). Each object made this way is an instance of the class. - A factory is a plain function that builds an object and returns it, like
createCounter("factory").
A class is a good fit when:
- you make many objects that share the same methods. The methods are stored once, on the class's prototype: a shared object where every instance looks for its methods.
- you want
instanceofto work.x instanceof Counteristruewhenxwas made bynew Counter(). - you want
#privatefields.
A factory is a good fit when:
- you want to hide data in a closure variable, like
let count = 0. - you want to build a different object depending on the arguments.
- you want to return a plain object that is easy to combine with others.
The playground below makes one counter each way. Press the button to run both.
function createCounter(label) { let count = 0; return { next() { count += 1; return label + ": " + count; }, };} class Counter { #count = 0; constructor(label) { this.label = label; } next() { this.#count += 1; return this.label + ": " + this.#count; }} const factoryCounter = createCounter("factory");const classCounter = new Counter("class");console.log(factoryCounter.next());console.log(classCounter.next());console.log(factoryCounter instanceof Counter);console.log(classCounter instanceof Counter);Both objects answer next() the same way. Only the class's object is an instance of Counter, so only it gives true for instanceof. The factory keeps count in a closure.
Both counters answer next() the same way: each prints its label and 1. The difference is in the last two lines. The factory's object is not an instance of Counter, so instanceof gives false. The class's object is, so it gives true.
function formatPrice(price) {
return "$" + price.toFixed(2);
}
console.log(formatPrice(3));Sometimes you need neither. formatPrice() turns a number into price text, like $3.00. It keeps no data between calls and has only one job. A plain function says exactly what it does. Wrapping it in a class would add extra code and give nothing useful back.
The table compares the two tools. Read each row as a question, then see how a factory and a class each answer it.
| What you want to know | With a factory | With a class |
|---|---|---|
| How do you make an object? | Call the function: createThing(options). | Use new: new Thing(options). |
| Where do the methods live? | Usually each object gets its own copies of the methods. | On the prototype. All instances share one copy of each method. |
| How do you hide data? | Keep it in a closure variable, like let count = 0. | Use a #private field, like #count. |
Do the methods use this? | They don't have to. They can use closure variables instead. | Usually yes. Methods read the object's data through this. |
Does instanceof work? | No. The object is a plain object, so instanceof Thing is false. | Yes. instanceof Thing is true for objects made with new Thing(). |
| How much memory do the methods use? | More, because each object may get new method functions. | Less, because all instances share the prototype's methods. |
Neither column wins. Use a class when shared methods and instanceof make the code clearer. Use a factory when you want a small function, closure variables, or an object that changes with the input. Either way, other code should see only a small public interface.
SOLID in practice
REAL REFACTORSOLID started with people who design big programs out of classes. Its ideas also help in JavaScript, if you keep them light. Treat each rule as a question you ask about your code:
- How many reasons does this piece of code have to change?
- Can I add a new kind of thing without editing the loop?
- Can I pass in a fake version of a tool when I test?
These questions help whether your objects come from classes, factories, or modules.
The table lists the five rules. Read each row as: the rule, the question it asks, and a small example.
| Rule | The question to ask | A small example |
|---|---|---|
| Single Responsibility | Does this object have just one main reason to change? | Split loading, adding up, and showing into separate pieces. |
| Open/Closed | Can I add a new kind of thing without editing a central list of if checks? | Add a Triangle with its own area(). The loop stays the same. |
| Liskov Substitution | Can a replacement object keep every promise of the one it replaces? | A Square whose setWidth() also changes the height breaks this rule. |
| Interface Segregation | Can the code ask for fewer methods? | Ask only for area(), not for a big object with many methods. |
| Dependency Inversion | Can this object be given its tools, instead of building them in? | Pass a storage object into the constructor. |
Start with Single Responsibility. It means each piece of code should have one job. Imagine a class that does four jobs:
- it loads an invoice from the server;
- it adds up the total;
- it formats the total as money text;
- it writes HTML into the page.
That class has four reasons to change. Split it into smaller pieces: a loader, a calculator, a formatter, and a renderer (the piece that shows things on the page). Each piece can be an object, a function, or a module. Small pieces are easier to test and to replace.
class InvoiceWidget { async show(id) { const invoice = await api.loadInvoice(id); const total = invoice.items.reduce((sum, item) => sum + item.price, 0); element.textContent = "Total: $" + total.toFixed(2); }}Line 3 loads the invoice; await waits for the server to answer. Line 4 adds up the prices. Line 5 writes the total into the page. That is three jobs, so the method has three reasons to change.
class InvoiceFormatter { total(invoice) { return invoice.items.reduce((sum, item) => sum + item.price, 0); }} class InvoiceRenderer { constructor(element, formatter) { this.element = element; this.formatter = formatter; } show(invoice) { const total = this.formatter.total(invoice); this.element.textContent = "Total: $" + total.toFixed(2); }}InvoiceFormatter only adds up the total. InvoiceRenderer only writes to the page. It gets the page element and the formatter through its constructor, on lines 8 to 10, instead of making them itself. Loading the invoice can live in a third piece.
Dependency Inversion is the rule that helps testing most in front-end code. A dependency is a tool your code needs, like a place to save data. Don't build the tool into the class. Pass it in from outside instead.
In the example below, the panel doesn't use localStorage (the browser's built-in storage) directly. It gets a storage object through its constructor:
- In the real app, you pass the browser's
localStorage. - In a test, you pass a small fake that keeps the data in a
Map.
The panel needs only two methods from it: setItem() and getItem(). Line 2 receives the storage, line 7 uses it, and line 22 passes in the fake.
class PreferencePanel { constructor(storage) { this.storage = storage; } chooseTheme(theme) { this.storage.setItem("theme", theme); return "Saved " + this.storage.getItem("theme"); }} const memoryStorage = new Map();const fakeStorage = { setItem(key, value) { memoryStorage.set(key, value); }, getItem(key) { return memoryStorage.get(key); },}; const panel = new PreferencePanel(fakeStorage);console.log(panel.chooseTheme("dark"));The panel uses whatever object it is given, as long as it has setItem() and getItem(). Here that is a fake storage built on a Map, so the result is "Saved dark".
chooseTheme("dark") prints Saved dark.- A
Thermostatkeeps#targetCelsiusprivate and checks every change. - A loop calls
shape.area()on circles, squares, and triangles. - A panel is given a
storageobject instead of usinglocalStoragedirectly. - A report class adds up totals and also writes HTML into the page.
- A factory keeps
countin a closure and returns anext()method. - A toolbar calls
item.save()without checking if the item is a draft, a note, or an image.
Put each card in the group it shows best. Some cards fit more than one idea. Pick the strongest one.
Liskov Substitution: the Square and Rectangle trap
STEP THROUGHThe Liskov Substitution rule is named after Barbara Liskov, the computer scientist who described it. It says: if code works with one kind of object, it should also work with a subtype of it, with no surprises. A subtype is a class that extends another class, like Square extends Rectangle.
The classic example looks harmless. A square sounds like a special kind of rectangle. But the two behave differently when you change their size:
- A rectangle has a width and a height. You can change one without the other.
- A square has one size. Its width and height must stay equal.
So if setWidth() on a square also changes the height, code written for rectangles gets a surprise. Step through the code below. makeWide() sets the width to 10 and returns the area. It expects the height to stay the same.
Step through the Square and Rectangle trap. The fix is not a cleverer extends. It is a smaller shared promise, like just area().
script
class Rectangle { constructor(width, height) { this.width = width; this.height = height; } setWidth(width) { this.width = width; } area() { return this.width * this.height; }} class Square extends Rectangle { constructor(size) { super(size, size); } setWidth(width) { this.width = width; this.height = width; }} function makeWide(shape) { shape.setWidth(10); return shape.area();} console.log(makeWide(new Square(4)));makeWide() calls setWidth(10) and expects the height to stay the same. The rectangle keeps its height and gives 50; the square changes its height too and gives 100.The rectangle gives 50 (10 × 5). The square gives 100 (10 × 10), because its height changed too. Code written for rectangles did not expect that.
The fix is not a cleverer extends. Instead, make the shared promise smaller. If the code only needs area(), both shapes can offer just that, with no inheritance at all:
- The rectangle keeps its
setWidth()method. - The square keeps its own rule: one size for every side.
Not every "a square is a rectangle" sentence should become extends in code.
class ResizableRectangle { constructor(width, height) { this.width = width; this.height = height; } setWidth(width) { this.width = width; } area() { return this.width * this.height; }} class SquareShape { constructor(size) { this.size = size; } area() { return this.size * this.size; }} const shapes = [new ResizableRectangle(10, 5), new SquareShape(4)];console.log(shapes.map((shape) => shape.area()).join(", "));Line 26 puts both shapes in one array. Line 27 calls area() on each one and prints 50, 16. Nothing changes a size it doesn't own.
class Receipt {
constructor(sender) {
this.sender = sender;
}
send(order) {
return this.sender.send("Receipt for " + order);
}
}
const email = {
send(message) {
return "Email: " + message;
},
};
console.log(new Receipt(email).send("Asha"));Composition means building an object out of other objects it is given, instead of inheriting from them. Receipt does not extend an email class. It gets a sender object in its constructor and calls this.sender.send(). Here the sender is email, so the last line prints Email: Receipt for Asha.
To send a text message instead, pass a different object that has a send() method. Receipt doesn't change.
Where you will use this
These ideas show up whenever a front-end program grows past one small script. For example:
- A form field object keeps its own checks and offers
isValid(). - Several payment methods all offer
charge(). The checkout code doesn't need to know if the user picked a card, a gift balance, or a saved wallet. - A table component is given a sorting function, instead of having one sort order built in.
- A test passes a fake storage, a fake clock, or a fake analytics tracker into a class that normally talks to the browser.
You will also learn to notice when OOP is too much:
- If a module holds one helper function, it doesn't need a class just to look official.
- If two objects share only a small bit of behavior, a mixin, a plain function, or delegation can be cleaner than inheritance.
For choosing between bigger patterns, Creational patterns and Structuring an application come in Stage 9. For now, pick the smallest public interface that makes change easier.
Before you add a class, write down the public methods you want other code to call. If the list is short and doesn't keep changing, a class or a factory can help. If the list keeps growing, split the jobs into smaller pieces first.
Common misconceptions
"OOP in JavaScript means copying the style of Java or C#."
JavaScript has its own tools: prototypes, functions you can pass around like values, closures, modules, and classes. Use the OOP ideas, not another language's extra code. A factory that hides data in a closure is just as much OOP as a class, as long as it returns an object with clear methods.
"Private fields make code secure."
A #private field stops normal JavaScript code outside the class from reading or changing it. That protects your design from mistakes. It does not keep secrets. Anyone with the source code, a browser extension, or the browser's developer tools can still see the data. Use private fields to keep your design honest, not to hide passwords or keys.
"Polymorphism needs inheritance."
It doesn't. The shapes loop only needs objects that have area() and draw(). They could come from classes that have nothing to do with each other, from factories, from object literals, or from mixins.
"SOLID means every tiny thing needs its own interface."
Adding layers too early makes code harder to read, not easier. Reach for SOLID when changing the code starts to hurt. Signs of that:
- one class with too many jobs;
- the same
ifchecks repeated in many places; - browser tools like
localStorageused directly everywhere; - tests that need the whole app to run.
"If a square is a kind of rectangle, Square should extend Rectangle."
The Square and Rectangle example shows the trap. What an object does matters more than what it is called. If a subclass can't keep every promise of the class it extends, use composition or a smaller shared interface instead.
Practice: design with objects
5 EXERCISESRead the Hexagon class. It has the same two methods as the other shapes, so the loop can use it. Run the code and type the exact text it prints.
class Hexagon {
constructor(side) {
this.side = side;
}
area() {
return ((3 * Math.sqrt(3)) / 2) * this.side ** 2;
}
draw() {
return "⬡ hexagon side " + this.side;
}
}
const shapes = [new Hexagon(2)];
for (const shape of shapes) {
console.log(shape.draw() + " area " + shape.area().toFixed(2));
}class Hexagon {
constructor(side) {
this.side = side;
}
area() {
return ((3 * Math.sqrt(3)) / 2) * this.side ** 2;
}
draw() {
return "⬡ hexagon side " + this.side;
}
}
const shapes = [new Hexagon(2)];
for (const shape of shapes) {
console.log(shape.draw() + " area " + shape.area().toFixed(2));
}The loop only calls draw() and area(). Hexagon has both, so the loop prints ⬡ hexagon side 2 area 10.39 with no new if check.
Split InvoiceWidget into at least two smaller classes. Then compare your answer with the worked solution. This exercise is about design, so there is no answer box.
class InvoiceWidget {
async show(id) {
const invoice = await api.loadInvoice(id);
const total = invoice.items.reduce((sum, item) => sum + item.price, 0);
element.textContent = "Total: $" + total.toFixed(2);
}
}class InvoiceFormatter {
total(invoice) {
return invoice.items.reduce((sum, item) => sum + item.price, 0);
}
}
class InvoiceRenderer {
constructor(element, formatter) {
this.element = element;
this.formatter = formatter;
}
show(invoice) {
this.element.textContent = "Total: $" + this.formatter.total(invoice).toFixed(2);
}
}The formatter holds the maths. The renderer holds the page writing. Loading the invoice can live in a third piece. Now each piece has one clear reason to change.
Run the code. What does the test with the fake storage print?
class PreferencePanel {
constructor(storage) {
this.storage = storage;
}
chooseTheme(theme) {
this.storage.setItem("theme", theme);
return this.storage.getItem("theme");
}
}
const memory = new Map();
const fakeStorage = {
setItem(key, value) { memory.set(key, value); },
getItem(key) { return memory.get(key); },
};
const panel = new PreferencePanel(fakeStorage);
console.log(panel.chooseTheme("dark"));class PreferencePanel {
constructor(storage) {
this.storage = storage;
}
chooseTheme(theme) {
this.storage.setItem("theme", theme);
return this.storage.getItem("theme");
}
}
const memory = new Map();
const fakeStorage = {
setItem(key, value) { memory.set(key, value); },
getItem(key) { return memory.get(key); },
};
const panel = new PreferencePanel(fakeStorage);
console.log(panel.chooseTheme("dark"));The panel doesn't know or care whether it got localStorage or a fake. So the test prints dark without using any browser storage.
makeWide() was written for rectangles. What number does it print for a Square? That number shows the surprise.
class Rectangle {
constructor(width, height) { this.width = width; this.height = height; }
setWidth(width) { this.width = width; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
constructor(size) { super(size, size); }
setWidth(width) { this.width = width; this.height = width; }
}
function makeWide(shape) {
shape.setWidth(10);
return shape.area();
}
console.log(makeWide(new Square(4)));The output is 100. makeWide() expects only the width to change. But Square.setWidth() changes the height too, so the square broke the rectangle's promise.
You need thousands of objects with the same methods. You want them to share one copy of each method, not get new copies each time. Which tool would you try first? Type factory or class.
Start with class. Its methods are stored once, on the prototype, and every instance shares them. instanceof works too. A factory is still a good choice when hiding data in a closure, or building different objects for different inputs, matters more.
Quiz: check your understanding
7 QUESTIONSRead the explanations even when you get it right. The wrong choices are common design mistakes.
Question 1 of 7Which pair best describes encapsulation and abstraction?
Choose an answer to see the explanation.
Question 2 of 7What does this shape code print?
Read the code, then predictclass Circle { area() { return 12; } } class Square { area() { return 9; } } const shapes = [new Circle(), new Square()]; console.log(shapes.map((shape) => shape.area()).join(","));Choose an answer to see the explanation.
Question 3 of 7When is a factory a good fit?
Choose an answer to see the explanation.
Question 4 of 7What does this thermostat code print?
Read the code, then predictclass Thermostat { #target = 20; setTarget(value) { if (value > 35) return "refused"; this.#target = value; return "saved"; } get display() { return this.#target; } } const t = new Thermostat(); console.log(t.setTarget(41)); console.log(t.display);Choose an answer to see the explanation.
Question 5 of 7Which change follows the Open/Closed rule?
Choose an answer to see the explanation.
Question 6 of 7What does this counter code print?
Read the code, then predictfunction createCounter() { let count = 0; return { next() { count += 1; return count; } }; } const a = createCounter(); const b = createCounter(); console.log(a.next(), a.next(), b.next());Choose an answer to see the explanation.
Question 7 of 7Which design is easiest to test?
Choose an answer to see the explanation.
Key takeaways
- Encapsulation protects an object's data. Every change goes through public methods, which can check the rules first.
- Abstraction gives other code a simple idea to use, like
setTarget(22), and hides the steps behind it. - Polymorphism lets one method call work on different objects. That is how you add new kinds of things without editing old code (Open/Closed).
- Factories and classes both work. Choose by what you need: closure variables, shared prototype methods,
this,instanceof, and clear code. - Use SOLID as five questions when you change code: one job per piece, no growing lists of
ifchecks, replacements that keep their promises, small interfaces, and tools passed in from outside.
Remember the one-liner.
OOP in JavaScript means giving each object a small list of methods, hiding the parts that break easily, and letting other code ask for what it wants instead of reaching inside.
Up next: Property flags & descriptors. You'll look at a single object property up close, and learn the settings that control whether it can be changed, listed, or deleted.