Table of Contents
- What is Object-Oriented Programming (OOP)?
- Core Concepts of OOP in JavaScript
- Objects in JavaScript
- Constructors and Prototypes
- ES6 Classes: Syntactic Sugar
- Inheritance in JavaScript
- Practical Example: A Shape Hierarchy
- Common Pitfalls and Best Practices
- Conclusion
- References
What is Object-Oriented Programming (OOP)?
Object-Oriented Programming (OOP) is a programming paradigm centered around objects rather than functions or logic. An object is a collection of data (properties) and behavior (methods) that act on that data. OOP aims to model real-world entities (e.g., a user, a car, a bank account) by bundling their attributes and actions into reusable, modular units.
Unlike class-based OOP languages (e.g., Java, Python), JavaScript uses a prototype-based model. This means there are no “classes” in the traditional sense (though ES6 introduced class syntax as syntactic sugar). Instead, objects inherit directly from other objects via a “prototype” chain.
Core Concepts of OOP in JavaScript
Encapsulation
Encapsulation is the practice of bundling data (properties) and methods that operate on that data into a single unit (an object), while restricting access to some of the object’s components. This hides internal state and exposes only necessary functionality, reducing complexity and preventing unintended side effects.
In JavaScript, encapsulation can be achieved using:
- Object literals (grouping properties/methods).
- Closures (to hide private variables).
- ES6 Symbols or WeakMaps (to simulate private properties).
Example with closures (private data):
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable (only accessible via methods)
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.getBalance(); // 150
account.balance; // undefined (private!)
Abstraction
Abstraction focuses on hiding unnecessary implementation details and exposing only essential features. It simplifies complex systems by providing a high-level interface.
For example, a Car object might expose startEngine() and drive() methods without revealing how the engine ignites or the transmission works.
Example:
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
this.isEngineRunning = false;
}
startEngine() {
this.isEngineRunning = true;
console.log(`${this.make} ${this.model} engine started.`);
}
drive() {
if (!this.isEngineRunning) {
this.startEngine(); // Abstracts the need to call startEngine() first
}
console.log(`${this.make} ${this.model} is driving.`);
}
}
const myCar = new Car("Tesla", "Model 3");
myCar.drive(); // Output: "Tesla Model 3 engine started." followed by "Tesla Model 3 is driving."
Inheritance
Inheritance allows objects to reuse properties and methods from other objects, promoting code reuse and establishing relationships between entities (e.g., a Dog inherits from an Animal).
In JavaScript, inheritance is实现 via the prototype chain. Every object has a [[Prototype]] (or __proto__), which references another object. When accessing a property/method, JavaScript first checks the object itself; if not found, it traverses the prototype chain until it finds the property or reaches null.
Polymorphism
Polymorphism means “many forms.” It allows objects of different types to be treated uniformly through a common interface. In practice, this often involves overriding methods in child objects to provide type-specific behavior.
Example:
// Parent object
const Animal = {
speak() {
return "Generic animal sound";
}
};
// Child objects inheriting from Animal
const Dog = Object.create(Animal);
Dog.speak = () => "Woof!"; // Override speak()
const Cat = Object.create(Animal);
Cat.speak = () => "Meow!"; // Override speak()
// Polymorphic function
function makeAnimalSpeak(animal) {
console.log(animal.speak());
}
makeAnimalSpeak(Dog); // "Woof!"
makeAnimalSpeak(Cat); // "Meow!"
Objects in JavaScript
Creating Objects
JavaScript objects can be created in several ways:
1. Object Literal (Most Common)
const person = {
name: "Alice",
age: 30,
greet() {
return `Hello, I'm ${this.name}`; // 'this' refers to the object
}
};
2. Object Constructor
const car = new Object();
car.make = "Toyota";
car.model = "Camry";
car.year = 2023;
3. Object.create()
Creates a new object with a specified prototype.
const animal = { isAlive: true };
const dog = Object.create(animal); // dog's prototype is animal
dog.breed = "Labrador";
console.log(dog.isAlive); // true (inherited from animal)
Object Properties and Methods
- Properties: Key-value pairs storing data (e.g.,
name: "Alice"). - Methods: Functions stored as properties (e.g.,
greet() { ... }).
Property Types
- Data properties: Store values (default).
- Accessor properties: Use
get/setmethods to compute values dynamically.
Enumerability and Writable Flags
Properties have attributes like enumerable (whether they appear in loops) and writable (whether they can be modified). Use Object.defineProperty() to configure them:
const person = {};
Object.defineProperty(person, "name", {
value: "Bob",
writable: false, // Cannot be changed
enumerable: true // Appears in loops
});
person.name = "Alice"; // Fails (in strict mode, throws error)
console.log(person.name); // "Bob"
Accessor Properties (Getters and Setters)
Accessor properties use get and set to control reading/writing values, enabling validation or computed properties.
Example:
const rectangle = {
width: 10,
height: 20,
get area() {
return this.width * this.height; // Computed when accessed
},
set dimensions(newDimensions) {
[this.width, this.height] = newDimensions; // Validate input here
}
};
console.log(rectangle.area); // 200
rectangle.dimensions = [15, 25];
console.log(rectangle.area); // 375
Constructors and Prototypes
Before ES6, JavaScript used function constructors to create reusable object templates. When a function is called with new, it becomes a constructor, returning a new object with its own properties and methods inherited from the constructor’s prototype.
Function Constructors
A constructor function initializes object properties. By convention, constructor names are capitalized.
Example:
function Person(name, age) {
this.name = name; // 'this' refers to the new object being created
this.age = age;
}
// Add a method to the constructor's prototype (shared by all instances)
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}, ${this.age} years old.`;
};
// Create instances with 'new'
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);
console.log(alice.greet()); // "Hello, I'm Alice, 30 years old."
console.log(bob.greet()); // "Hello, I'm Bob, 25 years old."
The Prototype Object
Every function has a prototype property (an object) that is inherited by all instances created with new. This allows methods to be shared across instances, saving memory.
Person.prototypeis the prototype object foraliceandbob.- Instances access prototype methods via the
[[Prototype]]chain (exposed as__proto__in browsers).
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true (inherited from Object)
The Prototype Chain
When accessing a property/method on an object, JavaScript first checks the object itself. If not found, it traverses the prototype chain (via __proto__) until it finds the property or reaches null.
Example:
console.log(alice.toString()); // "[object Object]" (inherited from Object.prototype)
ES6 Classes: Syntactic Sugar
ES6 introduced class syntax, which simplifies working with prototypes. Despite looking like class-based OOP, it’s still prototype-based under the hood.
Class Declaration
class Person {
// Constructor initializes properties
constructor(name, age) {
this.name = name;
this.age = age;
}
// Instance method (added to prototype)
greet() {
return `Hello, I'm ${this.name}`;
}
// Static method (attached to the class, not instances)
static isAdult(age) {
return age >= 18;
}
}
const charlie = new Person("Charlie", 22);
console.log(charlie.greet()); // "Hello, I'm Charlie"
console.log(Person.isAdult(22)); // true
Constructor Method
The constructor is called when creating a new instance with new. It initializes instance properties. If omitted, a default constructor is used.
Instance and Static Methods
- Instance methods: Defined inside the class (e.g.,
greet()) and inherited by instances via the prototype. - Static methods: Defined with
static(e.g.,isAdult()) and belong to the class itself, not instances.
Inheritance in JavaScript
Prototype-Based Inheritance
Before ES6, inheritance was实现 by manually setting an object’s prototype.
Example:
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound.`;
};
// Child constructor
function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor (super)
this.breed = breed;
}
// Set Dog's prototype to inherit from Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix constructor reference
// Override speak()
Dog.prototype.speak = function() {
return `${this.name} barks.`;
};
const buddy = new Dog("Buddy", "Golden Retriever");
console.log(buddy.speak()); // "Buddy barks."
ES6 Class Inheritance with extends and super
ES6 simplifies inheritance with extends (to inherit from a parent class) and super (to call the parent constructor/methods).
Example:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
return `${this.name} barks.`; // Override parent method
}
}
const max = new Dog("Max", "Poodle");
console.log(max.speak()); // "Max barks."
console.log(max instanceof Animal); // true (inherited)
Practical Example: A Shape Hierarchy
Let’s build a simple hierarchy to demonstrate OOP concepts:
class Shape {
constructor(color) {
this.color = color;
}
// Abstract method (to be overridden)
getArea() {
throw new Error("Subclasses must implement getArea()");
}
// Shared method
getColor() {
return this.color;
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
getArea() {
return Math.PI * this.radius ** 2; // Override getArea()
}
}
class Square extends Shape {
constructor(color, sideLength) {
super(color);
this.sideLength = sideLength;
}
getArea() {
return this.sideLength ** 2; // Override getArea()
}
}
// Usage
const redCircle = new Circle("red", 5);
console.log(redCircle.getArea()); // ~78.54
console.log(redCircle.getColor()); // "red"
const blueSquare = new Square("blue", 4);
console.log(blueSquare.getArea()); // 16
Common Pitfalls and Best Practices
-
thisBinding Issues:
thisin methods refers to the instance when called on an object. Avoid losing context (e.g., when passing methods as callbacks). Use arrow functions orbind(). -
Forgetting
newwith Constructors:
Omittingnewwhen calling a constructor leads tothisbeing the global object (orundefinedin strict mode). -
Prototype Chain Misunderstandings:
Inherited properties are not copied to instances—they’re accessed via the chain. Modifying a prototype affects all instances. -
Private Properties:
ES6+ supports private fields with#(e.g.,#balance), but they’re truly private and not accessible via the prototype.
Conclusion
Object-Oriented JavaScript revolves around objects, prototypes, and inheritance. While ES6 class syntax simplifies the syntax, JavaScript remains prototype-based at its core. By mastering encapsulation, abstraction, inheritance, and polymorphism, you can write modular, reusable, and maintainable code.
Start small: build simple hierarchies (e.g., users, products) and experiment with prototypes. The more you practice, the more intuitive JavaScript’s OOP model will become!