Skip to main content

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:

  1. Encapsulation - Bundling data and methods that operate on that data within one unit
  2. Abstraction - Hiding complex implementation details and showing only the necessary features
  3. Inheritance - Creating new classes based on existing ones
  4. 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

javascript
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:

javascript
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:

javascript
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:

javascript
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

javascript
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:

javascript
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

javascript
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+)

javascript
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)

javascript
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:

javascript
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 in ShoppingCart
  • Abstraction: Methods like addItem and getTotalPrice 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

Exercises

  1. Create a BankAccount class with methods for deposit, withdraw, and checking balance. Include appropriate validation to prevent overdrafts.

  2. Extend the BankAccount class to create a SavingsAccount that has a minimum balance requirement and an interest rate.

  3. Create a Library class that manages a collection of Book objects. Implement methods for adding books, checking out books, and returning books.

  4. 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! :)