JavaScript OOP Introduction
What is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data and code. JavaScript, despite being primarily known as a prototype-based language, offers robust support for object-oriented programming. Understanding OOP principles in JavaScript will help you write more organized, maintainable, and reusable code.
In this lesson, we'll explore the fundamentals of OOP in JavaScript, looking at how objects work, what classes are, and how inheritance functions in this versatile language.
Core OOP Concepts in JavaScript
Object-oriented programming in JavaScript revolves around four fundamental principles:
- Encapsulation - Bundling data and methods that operate on that data within one unit
- Abstraction - Hiding complex implementation details and showing only the necessary features
- Inheritance - Creating new classes based on existing ones
- Polymorphism - The ability to present the same interface for different underlying forms
Let's start by examining the basic building block of JavaScript OOP: the object.
JavaScript Objects
In JavaScript, an object is a collection of properties, where each property is defined as a key-value pair. Properties can be values of any type, including functions (which are then called methods).
Creating Objects
There are several ways to create objects in JavaScript:
1. Object Literals
const person = {
firstName: "John",
lastName: "Doe",
age: 30,
greet: function() {
return `Hello, my name is ${this.firstName} ${this.lastName}`;
}
};
console.log(person.firstName); // Output: John
console.log(person.greet()); // Output: Hello, my name is John Doe
2. Constructor Functions
Before ES6, constructor functions were the primary way to create objects with similar structures:
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.greet = function() {
return `Hello, my name is ${this.firstName} ${this.lastName}`;
};
}
const john = new Person("John", "Doe", 30);
const jane = new Person("Jane", "Smith", 25);
console.log(john.greet()); // Output: Hello, my name is John Doe
console.log(jane.greet()); // Output: Hello, my name is Jane Smith
3. ES6 Classes
ES6 introduced class syntax, which is a more familiar way for developers from other programming languages to work with objects:
class Person {
constructor(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
greet() {
return `Hello, my name is ${this.firstName} ${this.lastName}`;
}
}
const john = new Person("John", "Doe", 30);
console.log(john.greet()); // Output: Hello, my name is John Doe
The this
Keyword
In JavaScript OOP, the this
keyword refers to the object that is executing the current function. Its value depends on how the function is called:
const person = {
name: "Alice",
sayName() {
console.log(this.name);
}
};
person.sayName(); // Output: Alice
const sayNameFunction = person.sayName;
sayNameFunction(); // Output: undefined (or error in strict mode)
When sayNameFunction()
is called directly, this
no longer refers to the person
object, resulting in undefined. To maintain the correct context, you can use methods like bind()
, arrow functions, or the this
value from the enclosing scope.
Prototypes and Inheritance
JavaScript implements inheritance through prototypes. Every JavaScript object has a prototype property that refers to another object, creating a prototype chain.
Prototypal Inheritance
function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function() {
return "Some generic sound";
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Override the makeSound method
Dog.prototype.makeSound = function() {
return "Woof!";
};
const dog = new Dog("Rex", "German Shepherd");
console.log(dog.name); // Output: Rex
console.log(dog.makeSound()); // Output: Woof!
ES6 Class Inheritance
ES6 makes inheritance more straightforward with the extends
keyword:
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
return "Some generic sound";
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call the parent constructor
this.breed = breed;
}
makeSound() {
return "Woof!";
}
}
const dog = new Dog("Rex", "German Shepherd");
console.log(dog.name); // Output: Rex
console.log(dog.makeSound()); // Output: Woof!
Encapsulation in JavaScript
Encapsulation is about bundling data and methods together and restricting direct access to some components. JavaScript has several ways to implement encapsulation:
Using Closures for Private Variables
function Counter() {
// Private variable
let count = 0;
// Public methods
this.increment = function() {
count++;
return count;
};
this.decrement = function() {
count--;
return count;
};
this.getValue = function() {
return count;
};
}
const counter = new Counter();
console.log(counter.getValue()); // Output: 0
console.log(counter.increment()); // Output: 1
console.log(counter.increment()); // Output: 2
console.log(counter.decrement()); // Output: 1
console.log(counter.count); // Output: undefined (private variable)
Using Symbols for Private Properties (ES6+)
const _count = Symbol('count');
class Counter {
constructor() {
this[_count] = 0;
}
increment() {
this[_count]++;
return this[_count];
}
decrement() {
this[_count]--;
return this[_count];
}
getValue() {
return this[_count];
}
}
const counter = new Counter();
console.log(counter.getValue()); // Output: 0
console.log(counter.increment()); // Output: 1
console.log(counter[_count]); // Output: 1 (but the name of the symbol is not easily discoverable)
Using Private Fields (ES2022)
class Counter {
#count = 0; // Private field
increment() {
this.#count++;
return this.#count;
}
decrement() {
this.#count--;
return this.#count;
}
getValue() {
return this.#count;
}
}
const counter = new Counter();
console.log(counter.getValue()); // Output: 0
console.log(counter.increment()); // Output: 1
// console.log(counter.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class
Real-World Application: Building a Shopping Cart System
Let's apply OOP principles to create a simple shopping cart system:
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class CartItem {
constructor(product, quantity = 1) {
this.product = product;
this.quantity = quantity;
}
getTotalPrice() {
return this.product.price * this.quantity;
}
}
class ShoppingCart {
#items = [];
addItem(product, quantity = 1) {
const existingItem = this.#items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.#items.push(new CartItem(product, quantity));
}
}
removeItem(productId) {
this.#items = this.#items.filter(item => item.product.id !== productId);
}
updateQuantity(productId, quantity) {
const item = this.#items.find(item => item.product.id === productId);
if (item) {
item.quantity = quantity;
}
}
getItems() {
return [...this.#items];
}
getTotalPrice() {
return this.#items.reduce((total, item) => total + item.getTotalPrice(), 0);
}
clearCart() {
this.#items = [];
}
}
// Usage:
const laptop = new Product(1, "Laptop", 999.99);
const phone = new Product(2, "Smartphone", 499.99);
const cart = new ShoppingCart();
cart.addItem(laptop);
cart.addItem(phone, 2);
console.log("Cart items:", cart.getItems());
console.log("Total price: $" + cart.getTotalPrice().toFixed(2));
// Output: Total price: $1999.97
cart.updateQuantity(1, 2);
console.log("Updated total price: $" + cart.getTotalPrice().toFixed(2));
// Output: Updated total price: $2999.96
cart.removeItem(2);
console.log("After removal total price: $" + cart.getTotalPrice().toFixed(2));
// Output: After removal total price: $1999.98
This example demonstrates:
- Encapsulation: Private
#items
array inShoppingCart
- Abstraction: Methods like
addItem
andgetTotalPrice
hide implementation details - Inheritance: Although not explicitly shown, the system is designed so that it could be extended
- Objects and Classes: Using ES6 class syntax to create organized structures
Summary
Object-Oriented Programming in JavaScript provides a powerful way to structure your code, making it more maintainable, reusable, and organized. Key aspects we've covered include:
- Objects as collections of key-value pairs
- Multiple ways to create objects: object literals, constructor functions, and ES6 classes
- The importance of the
this
keyword in JavaScript OOP - Inheritance through prototypes and ES6 class extension
- Encapsulation techniques to protect data
- A practical example showing how OOP principles work together
JavaScript's approach to OOP is flexible, allowing you to choose the style that best fits your project needs, from the more traditional class-based approach to the prototype-based approach that is native to JavaScript.
Additional Resources
- MDN Web Docs: Object-oriented JavaScript for beginners
- JavaScript.info: Classes
- MDN Web Docs: Inheritance and the prototype chain
Exercises
-
Create a
BankAccount
class with methods fordeposit
,withdraw
, and checkingbalance
. Include appropriate validation to prevent overdrafts. -
Extend the
BankAccount
class to create aSavingsAccount
that has a minimum balance requirement and an interest rate. -
Create a
Library
class that manages a collection ofBook
objects. Implement methods for adding books, checking out books, and returning books. -
Build a simple inheritance hierarchy for different types of vehicles (Car, Motorcycle, Bicycle) that inherit from a base
Vehicle
class, with appropriate properties and methods for each.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)