Mateen Kiani
Published on Tue Jun 24 2025·6 min read
JavaScript has changed a lot since its early days, but one thing has stayed true: it still relies on objects. We often talk about functions, callbacks, and promises, yet we skip over how deep the object system really goes. Can JavaScript offer the same object oriented structure we see in classic languages like Java or C#?
It can, once you see how prototype chains, constructor functions, and ES6 classes fit together. By learning how these parts interact, you can write cleaner code, prevent hidden bugs, and choose the right approach for your project. The next sections will guide you from basic objects to advanced design patterns, so you feel confident building any feature with JavaScript.
At its core, JavaScript is built around objects. An object is a collection of key-value pairs where keys are strings or symbols. You can create an object in several ways:
const user = { name: 'Alice', age: 30 }
Object.create
, which sets up the prototype:const base = { greet() { return 'hi' } }const person = Object.create(base)person.name = 'Bob'
Tip: You can convert an object to a JSON string or parse JSON into an object. See more on JSON.
Objects hold data and behavior. Each object has an internal link to a prototype. This link, called [[Prototype]]
, determines what properties and methods are available if you don't define them directly on the object.
Understanding this link is key before you move on to classes. It also explains why adding or changing a method on one object can affect others created from the same prototype.
JavaScript uses prototype based inheritance instead of classes at a low level. When you access a property, the engine looks on the object itself. If it does not find it, it follows the prototype chain. This chain can go on until it hits Object.prototype
or null.
Here is how prototype inheritance works:
function Animal(type) {this.type = type}Animal.prototype.speak = function() {return `I am a ${this.type}`}const dog = new Animal('dog')console.log(dog.speak()) // I am a dog
In this example, speak
is not on dog
, but on Animal.prototype
. The new
keyword links dog
to that prototype. This setup defines shared behavior without copying functions to each instance.
Tip: Use
Object.getPrototypeOf(obj)
to inspect an object’s prototype at runtime. It helps debug inheritance chains.
Prototype chains are flexible. You can swap prototypes, extend built ins, or even create custom chains. But with great power comes great responsibility. If you overuse prototypes, your code can become hard to follow.
With ES6, JavaScript introduced a class
syntax that looks more familiar to developers from classical OOP languages. Under the hood, it still uses prototypes, but the code reads cleaner:
class User {constructor(name) {this.name = name}greet() {return `Hello, ${this.name}`}}const u = new User('Eve')console.log(u.greet()) // Hello, Eve
Classes let you define constructors and methods in one place. You can also use extends
and super
for inheritance:
class Admin extends User {constructor(name, role) {super(name)this.role = role}getRole() {return this.role}}
Tip: Even with classes, remember prototypes drive the actual inheritance. You can still use
Object.setPrototypeOf
if you need to tweak links.
ES6 classes group related logic and make patterns like factory or singleton easier to read. They also work nicely with tools like Babel and TypeScript.
One common OOP idea is to hide internal details. JavaScript did not offer private properties until recently. Now you can use private fields with the #
syntax:
class Counter {#count = 0increment() {this.#count++}getCount() {return this.#count}}const c = new Counter()c.increment()console.log(c.getCount()) // 1
Before private fields, developers used closures and modules to hide data:
function createBankAccount() {let balance = 0return {deposit(amount) {balance += amount},getBalance() {return balance}}}
Tip: Use modules or closures for older environments that do not support private fields.
Encapsulation prevents outside code from changing internal state in unexpected ways. It leads to more robust APIs and safer libraries.
Design patterns give you a template for common problems. In JavaScript, you can adapt patterns from other languages or invent new ones. Here are a few:
new
.// Module pattern exampleconst Logger = (function() {let logs = []function log(message) {logs.push({ message, timestamp: Date.now() })}function getLogs() {return logs}return { log, getLogs }})()Logger.log('start')console.log(Logger.getLogs())
These patterns make code predictable and easier to maintain. They also let teams share a common vocabulary.
Tip: Choose a pattern that fits your project size and team. Avoid overengineering small scripts.
Many frameworks let you use OOP principles. For example, in React you can write class components:
import React, { Component } from 'react'class App extends Component {render() {return <div>Welcome {this.props.name}</div>}}
But hooks and functions are now more common. Even then, you can group logic into custom hooks to mimic encapsulation:
import { createContext, useContext, useState } from 'react'const AuthContext = createContext()export function AuthProvider({ children }) {const [user, setUser] = useState(null)return (<AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>)}export function useAuth() {return useContext(AuthContext)}
Tip: See more on React Context for a deep dive on context.
Frameworks like Angular or Vue use classes, decorators, or reactive data. OOP ideas help organize components, services, and modules into coherent systems.
JavaScript is truly object oriented at its heart. With prototypes as the base, ES6 classes offer a friendly syntax, and private fields or closures bring encapsulation. Design patterns help you structure code and frameworks let you apply OOP to real apps. When you understand how each piece works, you can avoid traps like unexpected prototype changes or leaky data. Start with object literals, learn the prototype chain, and then explore classes. Try encapsulating logic in modules or hooks. With these tools in hand, you can build robust, maintainable code. Embrace JavaScript OOP and see your projects become clearer and more scalable.