Table of Contents
- What is Object-Oriented Programming?
- Core Principles of OOP
- Encapsulation
- Inheritance
- Polymorphism
- Abstraction
- JavaScript and OOP: A Unique Approach
- Objects in JavaScript: The Building Blocks
- Constructors and the
newKeyword - ES6 Classes: Syntactic Sugar Over Prototypes
- Prototypal Inheritance: How Objects Inherit
- Encapsulation in JavaScript
- Polymorphism in JavaScript
- Abstraction in JavaScript
- Practical Example: Building a Library Management System
- Conclusion
- References
1. What is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a design philosophy that organizes code into objects, which are collections of:
- Properties: Data or attributes that describe the object (e.g., a
carobject might havecolor,model, andyear). - Methods: Functions that define the object’s behavior (e.g.,
car.drive(),car.brake()).
OOP aims to model real-world systems by breaking them into reusable, self-contained objects. For example, in a banking app, you might have Account objects with properties like balance and methods like deposit() or withdraw().
Why OOP?
- Reusability: Objects can be reused across projects or parts of an application.
- Maintainability: Changes to an object’s internal logic don’t affect other parts of the code (encapsulation).
- Scalability: Complex systems are easier to manage when broken into modular objects.
2. Core Principles of OOP
OOP is built on four foundational principles. Let’s define them and preview how JavaScript implements each:
2.1 Encapsulation
Definition: Bundling data (properties) and methods that operate on that data into a single unit (object), while restricting access to some of the object’s components.
Goal: Protect data from unintended modification and expose only necessary functionality.
JavaScript Implementation: Uses closures, private fields (ES6+), or naming conventions (e.g., _property) to hide internal state.
2.2 Inheritance
Definition: Allowing objects to inherit properties and methods from other objects, promoting code reuse.
Goal: Avoid redundant code by defining shared behavior in a “parent” object and having “child” objects inherit from it.
JavaScript Implementation: Prototypal inheritance (objects inherit directly from other objects) instead of class-based inheritance (common in Java/C#).
2.3 Polymorphism
Definition: The ability of different objects to respond to the same method name in different ways.
Goal: Write flexible code that works with multiple object types without needing to know their specific type.
JavaScript Implementation: Method overriding (child objects redefine parent methods) and dynamic typing.
2.4 Abstraction
Definition: Hiding complex implementation details and exposing only essential features.
Goal: Simplify interaction with objects by focusing on “what” they do, not “how” they do it.
JavaScript Implementation: Using classes or factory functions to define abstract interfaces (e.g., a Shape class with an abstract area() method that subclasses must implement).
3. JavaScript and OOP: A Unique Approach
Unlike class-based languages (e.g., Java), JavaScript is prototype-based. This means:
- There are no “classes” in the traditional sense (though ES6 introduced
classsyntax as syntactic sugar). - Objects inherit directly from other objects (called “prototypes”), not from classes.
- Every object has a
prototype(a hidden link to another object), forming a prototype chain. If a property/method isn’t found on an object, JavaScript searches its prototype, and so on, until the chain ends.
4. Objects in JavaScript: The Building Blocks
In JavaScript, almost everything is an object (strings, arrays, functions, etc.). Even primitives (like 123 or "hello") are wrapped in temporary objects when accessing methods.
4.1 Object Literals
The simplest way to create an object is with object literal syntax ({}):
// An object representing a book
const book = {
title: "The Great Gatsby",
author: "F. Scott Fitzgerald",
pages: 180,
isAvailable: true,
// Method: describes behavior
getSummary() {
return `${this.title} by ${this.author}, ${this.pages} pages.`;
},
// Accessor method (getter)
get availability() {
return this.isAvailable ? "Available" : "Borrowed";
}
};
console.log(book.getSummary()); // "The Great Gatsby by F. Scott Fitzgerald, 180 pages."
console.log(book.availability); // "Available"
Key Notes:
thisrefers to the object itself (e.g.,this.titleaccesses thetitleproperty ofbook).- Methods are functions defined as object properties.
- Getters/setters (e.g.,
availability) allow controlled access to properties.
5. Constructors and the new Keyword
To create multiple objects with the same structure (e.g., multiple Book instances), use constructor functions. These are functions designed to initialize objects.
Example: Constructor Function
// Constructor function for Book
function Book(title, author, pages) {
// Properties initialized with parameters
this.title = title;
this.author = author;
this.pages = pages;
this.isAvailable = true; // Default value
// Method (problem: recreated for every instance!)
this.getSummary = function() {
return `${this.title} by ${this.author}`;
};
}
// Create instances with the `new` keyword
const book1 = new Book("1984", "George Orwell", 328);
const book2 = new Book("To Kill a Mockingbird", "Harper Lee", 336);
console.log(book1.getSummary()); // "1984 by George Orwell"
console.log(book2.title); // "To Kill a Mockingbird"
How new Works:
- Creates a new empty object.
- Sets the new object’s prototype to the constructor’s
prototypeproperty. - Binds
thisto the new object and runs the constructor. - Returns the new object (unless the constructor explicitly returns another object).
6. ES6 Classes: Syntactic Sugar Over Prototypes
ES6 (2015) introduced class syntax to simplify object creation and inheritance. Despite the name, JavaScript classes are not traditional classes—they are just syntactic sugar over prototype-based inheritance.
Example: ES6 Class
class Book {
// Constructor initializes properties
constructor(title, author, pages) {
this.title = title;
this.author = author;
this.pages = pages;
this.isAvailable = true;
}
// Method (added to the prototype, shared by all instances)
getSummary() {
return `${this.title} by ${this.author}`;
}
// Static method (belongs to the class, not instances)
static comparePages(book1, book2) {
return `${book1.title} has ${book1.pages} pages; ${book2.title} has ${book2.pages} pages.`;
}
}
// Create instances
const book1 = new Book("1984", "George Orwell", 328);
const book2 = new Book("To Kill a Mockingbird", "Harper Lee", 336);
console.log(book1.getSummary()); // "1984 by George Orwell"
console.log(Book.comparePages(book1, book2)); // "1984 has 328 pages; To Kill a Mockingbird has 336 pages."
Key Notes:
constructor: Runs when a new instance is created (initializes properties).- Methods like
getSummaryare added to the class’sprototype, so all instances share them (unlike constructor functions, which recreate methods per instance). staticmethods (e.g.,comparePages) are called on the class itself, not instances.
7. Prototypal Inheritance: How Objects Inherit
In JavaScript, inheritance is implemented via the prototype chain. Every object has a prototype (a reference to another object). When you access a property/method on an object, JavaScript first checks the object itself. If not found, it searches the prototype, then the prototype’s prototype, and so on.
Example: Prototypal Inheritance with extends
ES6 class syntax simplifies inheritance with extends and super:
// Parent class
class Animal {
constructor(name) {
this.name = name;
this.isAlive = true;
}
eat() {
return `${this.name} is eating.`;
}
}
// Child class inheriting from Animal
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() { // Child-specific method
return `${this.name} barks: Woof!`;
}
// Override parent method
eat() {
return `${this.name} (a ${this.breed}) is eating kibble.`;
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.name); // "Buddy" (inherited from Animal)
console.log(myDog.bark()); // "Buddy barks: Woof!" (child method)
console.log(myDog.eat()); // "Buddy (a Golden Retriever) is eating kibble." (overridden)
Prototype Chain in Action:
myDog → Dog.prototype → Animal.prototype → Object.prototype → null (end of chain).
8. Encapsulation in JavaScript
Encapsulation ensures internal state is hidden, and only public methods can modify it. JavaScript has evolved to support true encapsulation:
1. Naming Conventions (Weak Encapsulation)
Use a leading underscore (_) to signal “private” properties (convention only—still accessible):
class Counter {
constructor() {
this._count = 0; // "Private" by convention
}
increment() {
this._count++;
}
getCount() {
return this._count;
}
}
const counter = new Counter();
counter._count = 100; // Still possible (no true privacy)
2. ES6 Private Fields (True Encapsulation)
Use # to define private fields (ES2022+), which are inaccessible from outside the class:
class Counter {
#count = 0; // Private field (truly inaccessible)
increment() {
this.#count++;
}
getCount() {
return this.#count;
}
}
const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // 1
counter.#count = 100; // Error: Private field '#count' must be declared in an enclosing class
9. Polymorphism in JavaScript
Polymorphism allows objects of different types to respond to the same method name. In JavaScript, this is enabled by dynamic typing and method overriding.
Example: Polymorphism with draw()
class Shape {
draw() {
return "Drawing a shape.";
}
}
class Circle extends Shape {
draw() {
return "Drawing a circle with a radius.";
}
}
class Square extends Shape {
draw() {
return "Drawing a square with sides.";
}
}
// Polymorphic function: works with any Shape
function renderShape(shape) {
console.log(shape.draw());
}
// Pass different Shape types to renderShape
renderShape(new Shape()); // "Drawing a shape."
renderShape(new Circle()); // "Drawing a circle with a radius."
renderShape(new Square()); // "Drawing a square with sides."
Here, renderShape works with Shape, Circle, and Square because they all implement draw().
10. Abstraction in JavaScript
Abstraction hides complexity by exposing only essential features. In JavaScript, you can define abstract “interfaces” using classes with unimplemented methods (subclasses must implement them).
Example: Abstract Shape Class
class Shape {
// Abstract method (subclasses must implement)
area() {
throw new Error("Subclasses must implement the 'area' method.");
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() { // Implement abstract method
return Math.PI * this.radius ** 2;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
area() { // Implement abstract method
return this.side ** 2;
}
}
const circle = new Circle(5);
console.log(circle.area()); // ~78.54
const square = new Square(4);
console.log(square.area()); // 16
const shape = new Shape();
shape.area(); // Error: Subclasses must implement the 'area' method.
Here, Shape defines the abstract area() method, and Circle/Square implement it with their own logic.
11. Practical Example: Building a Library Management System
Let’s tie together OOP concepts with a simple library system. We’ll create:
- A
Bookclass with private fields and methods to borrow/return books. - A
Libraryclass to manage books.
class Book {
#title; // Private field
#author;
#isBorrowed = false; // Default: not borrowed
constructor(title, author) {
this.#title = title;
this.#author = author;
}
// Public method to borrow the book
borrow() {
if (this.#isBorrowed) {
return `${this.#title} is already borrowed.`;
}
this.#isBorrowed = true;
return `You borrowed ${this.#title}.`;
}
// Public method to return the book
return() {
if (!this.#isBorrowed) {
return `${this.#title} is not borrowed.`;
}
this.#isBorrowed = false;
return `You returned ${this.#title}.`;
}
// Public getter for title (read-only)
get title() {
return this.#title;
}
// Check availability
isAvailable() {
return !this.#isBorrowed;
}
}
class Library {
#books = []; // Private array to store books
addBook(book) {
if (book instanceof Book) {
this.#books.push(book);
return `Added "${book.title}" to the library.`;
}
return "Invalid book.";
}
listAvailableBooks() {
const available = this.#books.filter(book => book.isAvailable());
return available.map(book => book.title).join(", ");
}
}
// Usage
const library = new Library();
const book1 = new Book("1984", "George Orwell");
const book2 = new Book("To Kill a Mockingbird", "Harper Lee");
console.log(library.addBook(book1)); // "Added "1984" to the library."
console.log(library.addBook(book2)); // "Added "To Kill a Mockingbird" to the library."
console.log(book1.borrow()); // "You borrowed 1984."
console.log(library.listAvailableBooks()); // "To Kill a Mockingbird"
console.log(book1.return()); // "You returned 1984."
console.log(library.listAvailableBooks()); // "1984, To Kill a Mockingbird"
Key Concepts Demonstrated:
- Encapsulation:
Bookuses private fields (#title,#isBorrowed) and controls access via methods. - Abstraction:
Bookhides how#isBorrowedis modified (only viaborrow()/return()). - Reusability:
Librarymanages multipleBookinstances.
12. Conclusion
Object-Oriented Programming in JavaScript is a powerful paradigm built on prototypes, not classes. While ES6 class syntax simplifies syntax, under the hood, JavaScript relies on prototype chains for inheritance. By mastering core OOP principles—encapsulation, inheritance, polymorphism, and abstraction—you can write modular, reusable, and maintainable code.
Remember: JavaScript’s prototype-based model offers flexibility, but it’s critical to understand how the prototype chain works to avoid pitfalls. Practice with real-world examples (like the library system above) to solidify your skills!
13. References
- MDN Web Docs: JavaScript Objects
- MDN Web Docs: Classes
- MDN Web Docs: Inheritance and the Prototype Chain
- Kyle Simpson, You Don’t Know JS: this & Object Prototypes (O’Reilly Media)
- ECMAScript 2022 Specification (Private Fields)