Suggest an editImprove this articleRefine the answer for “OOP in JavaScript (Object-oriented programming)”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**OOP in JavaScript** organizes code into objects that share behavior through a prototype chain, not by copying state per instance. ```javascript class Animal { speak() { return 'sound'; } } class Dog extends Animal { speak() { return 'bark'; } } console.log(new Dog().speak()); // "bark" // dog -> Dog.prototype -> Animal.prototype ``` **Key point:** ES6 `class` is syntactic sugar over prototypes. The four principles are encapsulation, inheritance, polymorphism, and abstraction.Shown above the full answer for quick recall.Answer (EN)Image**OOP in JavaScript** organizes code into objects that bundle data and behavior, using a prototype chain for shared methods rather than copying state per instance. ## Theory ### TL;DR - Prototype chain is a lookup path: `dog -> Dog.prototype -> Animal.prototype -> Object.prototype`. First match wins. - ES6 `class` syntax is syntactic sugar over prototypes. Under the hood, nothing changed. - Four principles: encapsulation (hide internals), inheritance (share behavior), polymorphism (same interface, different implementation), abstraction (hide complexity) - Use classes for readable hierarchies, `Object.create()` for dynamic sharing, factory functions when inheritance is not needed ### Quick example ```javascript class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } class Dog extends Animal { speak() { return `${this.name} barks`; } // overrides parent method } const dog = new Dog('Rex'); console.log(dog.speak()); // "Rex barks" // Under the hood: // dog.__proto__ === Dog.prototype // Dog.prototype.__proto__ === Animal.prototype ``` `Dog` overrides `speak()` from `Animal`. When you call `dog.speak()`, the engine finds it on `Dog.prototype` first and stops. That is the prototype chain. ### How the prototype chain works When you access `dog.speak()`, V8 walks: the `dog` object itself, then `Dog.prototype`, then `Animal.prototype`, then `Object.prototype`, then null. First match wins. This is delegation, not copying. All `Dog` instances share one `speak` method on `Dog.prototype`. Java copies state into each instance. JavaScript does not. Classes give you a cleaner way to set up that chain, but the chain was always there. ### The four OOP principles **Encapsulation** hides internals and exposes a controlled interface. Private fields with `#` do this at the language level since ES2022: ```javascript class BankAccount { #balance = 0; constructor(initial) { this.#balance = initial; } deposit(amount) { if (amount > 0) this.#balance += amount; } getBalance() { return this.#balance; } } const account = new BankAccount(1000); account.deposit(500); console.log(account.getBalance()); // 1500 // account.#balance -> SyntaxError: private field outside class ``` **Inheritance** lets a child class reuse and extend a parent. The child must call `super()` before using `this`: ```javascript class Animal { constructor(name) { this.name = name; } move() { return `${this.name} moves`; } } class Dog extends Animal { constructor(name, breed) { super(name); // must come before this.breed this.breed = breed; } fetch() { return `${this.name} fetches the ball`; } } ``` **Polymorphism** means the same method name behaves differently depending on which object calls it: ```javascript class Shape { area() { throw new Error('area() not implemented'); } } class Circle extends Shape { constructor(r) { super(); this.r = r; } area() { return Math.PI * this.r ** 2; } } class Square extends Shape { constructor(s) { super(); this.s = s; } area() { return this.s ** 2; } } [new Circle(5), new Square(4)].forEach(s => console.log(s.area().toFixed(2))); // 78.54 // 16.00 ``` **Abstraction** hides complexity behind a simple interface. The caller does not need to know if it is MySQL or PostgreSQL, just call `query()`. ### Composition vs inheritance Inheritance works for true "is-a" relationships. A `Dog` is an `Animal`. But when you need to mix behaviors from multiple unrelated sources, inheritance breaks down. Composition builds objects by combining smaller pieces: ```javascript const canSwim = (state) => ({ swim: () => `${state.name} swims` }); const canFly = (state) => ({ fly: () => `${state.name} flies` }); const Duck = (name) => { const state = { name }; return Object.assign({}, canSwim(state), canFly(state)); }; const duck = Duck('Donald'); console.log(duck.swim()); // "Donald swims" console.log(duck.fly()); // "Donald flies" ``` Hierarchies deeper than 3-4 levels tend to become fragile. When you find yourself wanting `super.super`, that is a sign to flatten with composition. ### Prototypes before ES6 ES6 classes did not invent anything new. Here is the same pattern in ES5: ```javascript function Person(name) { this.name = name; } Person.prototype.greet = function() { return `Hi, I'm ${this.name}`; }; function Developer(name, lang) { Person.call(this, name); this.lang = lang; } Developer.prototype = Object.create(Person.prototype); Developer.prototype.constructor = Developer; Developer.prototype.code = function() { return `${this.name} writes ${this.lang}`; }; const dev = new Developer('Anna', 'JavaScript'); console.log(dev.greet()); // "Hi, I'm Anna" console.log(dev.code()); // "Anna writes JavaScript" ``` You will still see this in older codebases. The ES6 class version compiles to something close to this. ### When to use OOP - Shared state across multiple methods and a clear "is-a" relationship: classes with inheritance - Behavior from multiple unrelated sources: composition - No shared state, just grouped logic: factory functions or plain objects - More than 4-5 levels of inheritance: redesign with composition ### Common mistakes **Forgetting `super()` before `this` in a child constructor:** ```javascript // Wrong - throws ReferenceError class Child extends Parent { constructor() { this.prop = 'child'; } } // Right class Child extends Parent { constructor() { super(); this.prop = 'child'; } } ``` The parent constructor is responsible for creating the `this` object when you use `extends`. Until `super()` runs, `this` does not exist. **Mutating a shared prototype:** ```javascript const proto = { greet() { return `Hi, ${this.name}`; } }; const a = Object.create(proto); const b = Object.create(proto); proto.sayBye = () => 'Bye'; // both a and b now have sayBye console.log(a.sayBye()); // "Bye" console.log(b.sayBye()); // "Bye" ``` This is prototype pollution. The lodash CVE-2018-3721 exploited exactly this mechanism. Freeze shared objects with `Object.freeze(proto)` or avoid mutable shared prototypes entirely. **Private fields with older bundlers:** ```javascript class Secure { #secret = 42; } // Webpack below v5 cannot parse this ``` Fall back to `WeakMap` if you cannot update your toolchain: ```javascript const _secrets = new WeakMap(); class Secure { constructor() { _secrets.set(this, 42); } getSecret() { return _secrets.get(this); } } ``` **Class field arrows vs prototype methods:** ```javascript class Counter { count = 0; increment = () => { this.count++; }; // one function per instance (more memory) decrement() { this.count--; } // shared on prototype (cheaper) } ``` Neither is wrong. Arrow fields fix `this` binding when passing as callbacks but cost more memory per instance. Regular methods are cheaper but need explicit `.bind()` at the call site. **Deep prototype chains and lookup performance:** V8 inline caches work best with flat structures. Chains longer than 5 levels cause cache misses on property lookups. This matters in hot loops; in typical application code the difference is negligible. ### How V8 handles this internally V8 stores each object's `[[Prototype]]` as a hidden pointer. Property access triggers `OrdinaryGetPrototypeOf` traversal up the chain. V8 caches repeated lookups in hidden classes (shapes) for speed. `new Dog()` sets `[[Prototype]]` to `Dog.prototype` via `Object.setPrototypeOf`. That is all `class` does under the hood. ### Real-world usage - React: `class Component extends React.Component` for lifecycle methods. `PureComponent` is just `Component` with a custom `shouldComponentUpdate` on its prototype. - Node.js streams: `Readable`, `Transform`, `Duplex` all chain through prototypes. `Transform extends Readable`. - Express: middleware as prototype chains; `app.use()` delegates to a router prototype. - Lodash: utility mixins via `Object.assign(proto, mixins)` for shared methods. ### Follow-up questions **Q:** What is the difference between class fields and prototype methods? **A:** Class fields create per-instance own properties, so each object gets its own copy in memory. Prototype methods are shared: one function on the prototype, every instance uses the same one. **Q:** Why does `super()` have to come before `this` in a child constructor? **A:** When you use `extends`, the parent constructor is responsible for creating the `this` object. Until `super()` runs, `this` does not exist. Accessing it throws `ReferenceError`. **Q:** How does `Object.create(null)` differ from `{}`? **A:** `{}` has `Object.prototype` in its chain, so it inherits `toString`, `hasOwnProperty`, and the rest. `Object.create(null)` has no prototype at all. Useful for pure dictionaries where inherited keys would cause bugs. **Q:** Why does `instanceof` sometimes give wrong results with `Object.create()`? **A:** `instanceof` checks whether the constructor's `prototype` appears anywhere in the object's prototype chain. If you wire up a chain manually without connecting it to the constructor, `instanceof` will not find it. **Q:** (Senior) How would you implement private state without `#` fields, and what is the tradeoff? **A:** Use a `WeakMap` keyed by instance. Entries are garbage-collected when the instance dies, so no memory leak. Symbols as keys also work but are visible via `Object.getOwnPropertySymbols()`. The `#` syntax is truly private and V8 hashes field names to prevent collision across bundles. ## Examples ### Basic: prototype chain in action ```javascript class Vehicle { constructor(make) { this.make = make; } describe() { return `This is a ${this.make}`; } } class Car extends Vehicle { constructor(make, model) { super(make); this.model = model; } describe() { return `${super.describe()} ${this.model}`; // calls Vehicle.describe() } } const car = new Car('Toyota', 'Camry'); console.log(car.describe()); // "This is a Toyota Camry" // Verify the chain manually console.log(Object.getPrototypeOf(car) === Car.prototype); // true console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true ``` `super.describe()` walks up to `Vehicle.prototype` and calls the parent method from inside the override. The manual chain checks confirm exactly what ES6 `class` sets up for you. ### Intermediate: logger with composition ```javascript class Logger { constructor(prefix) { this.prefix = prefix; this.logs = []; } log(message) { const entry = `[${this.prefix}] ${message}`; this.logs.push(entry); console.log(entry); return entry; } getLogs() { return [...this.logs]; } // return a snapshot, not the original array } class UserService { constructor() { this.logger = new Logger('UserService'); // composition: has-a Logger } login(user) { return this.logger.log(`Login: ${user.name}`); } logout(user) { return this.logger.log(`Logout: ${user.name}`); } } const service = new UserService(); service.login({ name: 'Alice' }); service.logout({ name: 'Alice' }); console.log(service.logger.getLogs()); // ["[UserService] Login: Alice", "[UserService] Logout: Alice"] ``` `UserService` uses `Logger` through composition, not inheritance. If logging requirements change, you swap or extend `Logger` without touching `UserService`. I have seen this pattern replace entire inheritance trees in production codebases where someone force-fit inheritance just to share a logger. ### Advanced: prototype pollution ```javascript const sharedProto = { greet() { return `Hi, ${this.name}`; } }; const user1 = Object.create(sharedProto); user1.name = 'Bob'; const user2 = Object.create(sharedProto); user2.name = 'Carol'; // Mutating the shared prototype affects every object in the chain sharedProto.isAdmin = () => true; console.log(user1.isAdmin()); // true - expected console.log(user2.isAdmin()); // true - user2 never asked for this ``` This is how the lodash CVE-2018-3721 worked. An attacker-controlled JSON payload reached `__proto__` and injected properties onto `Object.prototype`, affecting every object in the Node.js process. Freeze shared prototypes with `Object.freeze(sharedProto)`, or use `Object.create(null)` for data containers that should not inherit anything.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.