Now that we know some basic terminology, let's look at some concepts that often arise in conjunction with Object-Oriented programming.
Encapsulation is one of the fundamental concepts of object-oriented programming. At its core, encapsulation describes the idea of bundling or combining data and the behaviors of that data into a single entity, e.g., an object. For example:
let cocoa = {
animalType: 'cat',
name: 'Cocoa',
purr: function() {
console.log('Purr');
},
info: function() {
console.log(`My name is ${this.name}. I am a ${this.animalType}.`)
}
}
cocoa.purr(); // Purr
cocoa.info(); // My name is Cocoa. I am a cat.
In this code, we've bundled the data (the values of the animalType
and name
properties) and behaviors (the purr
and info
methods) into a single object. We can say that the object referenced by cocoa
encapsulates information and behaviors about Cocoa the cat.
Another benefit of encapsulation is that it lets the programmer think about things with a new level of abstraction. Objects are represented as real-world nouns and can be given methods that describe the behavior provided for those objects.
Consider a simple banking application. At minimum, the code must contain data about bank accounts (account number, balance, account type) and users (name, address, phone number). The code also must provide behaviors or operations that use and manipulate that data. For instance, we should have operations that make withdrawals and deposit new funds.
let bankAccount = {
accountNumber: '1234567890',
balance: 10234.56,
accountType: 'checking',
name: 'Jane Doe',
address: '2246 NW 12th Ave, Portland, Oregon',
phone: '456-334-1221',
withdraw: function(amount) {
if (this.balance >= amount) {
this.balance -= amount;
return amount;
} else {
return 0;
}
},
deposit: function(amount) {
this.balance += amount;
},
}
One thing is evident here: the data and operations you perform on your data are related. It makes sense to withdraw money from a specific bank account. However, withdrawing money from 'Jane Doe'
, the account owner, doesn't quite make sense. Jane may have multiple accounts; we need to access a bankAccount
object, not a name
object.
Suppose our program keeps track of something and performs operations on the tracked data. It makes sense to combine the data and functionality into a single entity. That's what object-oriented programming is all about. We call this principle of combining data and operations on that data encapsulation. Encapsulation refers to bundling state (data) and behavior (operations) to form objects.
In most OOP languages, encapsulation has a broader purpose. It also refers to restricting access to state and certain behaviors; an object only exposes the data and behaviors that other parts of the application need to work. In other words, objects expose a public interface for interacting with other objects and keep their implementation details hidden. Thus, other objects can't change an object's data without going through the proper interface.
Until recently, JavaScript did not directly support access restrictions. In older versions of JavaScript, all object properties, be they instance variables or methods, were public. They provided no protections restricting access to just the object that contained them. This meant that any code that had access to an object could add, delete, or modify any properties of the object.
ECMAScript 2022, a.k.a. ES13, introduced the concept of private fields to JavaScript. Before that, data privacy could only be achieved through tortured syntax tricks, such as nested functions, nested objects, and IIFEs (Immediately Invoked Function Expressions). With the introduction of private fields, it's now possible to define "private" instance variables and instance methods for a type. These private fields enable a more flexible form of encapsulation that protects private data and methods from outside use.
Polymorphism is the ability of different data types to respond to a common interface. For instance, if we have a function or method that invokes the move
method on the object passed to it as an argument, we can pass the function or method any argument that has a compatible move
method. The object might represent a human, a cat, a jellyfish, or, conceivably, even a car, planet, or train. Polymorphism lets us use the same method invocation for many different object types. In such cases, the function or method is considered polymorphic.
let cat = {
move() {
console.log("The cat is walking");
},
};
let planet = {
move() {
console.log("The planet is revolving around the Sun");
},
}
for (let item of [cat, planet]) {
item.move();
}
// The cat is walking
// The planet is revolving around the Sun
In the above example, both the cat
and planet
objects have a move
method that takes no arguments. Since they have the same interface, we can call them without knowing which one we're working with. The for
loop, for example, calls the move
method on each of the objects. We can thus say that the cat
and planet
objects are polymorphic with respect to the move
method.
Poly stands for many and morph stands for forms.
In most OO languages, inheritance lets an object acquire (inherit) all the behaviors and properties of another object with a less specific type than the inheriting type. This allows programmers to define small, reusable, more specific classes for fine-grained, detailed behaviors.
For instance, since all plants photosynthesize, we might have a Plant
type that defines a photosynthesize
method. Types like Tree
, Flower
, and Grass
could then inherit from the Plant
type. Through inheritance, Tree
, Flower
, and Grass
objects can all photosynthesize
.
class Plant {
photosynthesize() {
console.log(`This ${this.constructor.name} is photosynthesizing`);
}
}
class Tree extends Plant {
}
class Flower extends Plant {
}
class Grass extends Plant {
}
let tree = new Tree();
tree.photosynthesize(); // This Tree is photosynthesizing
let flower = new Flower();
flower.photosynthesize(); // This Flower is photosynthesizing
let grass = new Grass();
grass.photosynthesize(); // This Grass is photosynthesizing
Don't worry overly much about the syntax of that example. For now, all you need to know is that the Tree
, Flower
, and Grass
classes (types) all inherit from the Plant
class. As a result, objects created from the those classes can all call the photosynthesize
method even though it's defined in the Plant
class.
As with the idea of classes, JavaScript has a non-traditional approach to inheritance. Since it doesn't support classic classes, it also doesn't support conventional inheritance. Instead, it uses something called prototypal inheritance that is behaviorally similar to traditional inheritance. In prototypal inheritance, each object in JavaScript links to a "prototype object" from which it inherits behaviors. Those prototype objects, in turn, inherit additional behaviors from other prototype objects.
In the example above, the classes shown are JavaScript classes, not classical classes. The inheritance shown in that example is prototypal inheritance, not traditional inheritance. We'll discuss JavaScript classes and prototype inheritance in depth later in this book. For now, you just need to understand that JavaScript's inheritance system is prototypal rather than class-based.
That was a high-speed trip through the most basic concepts of OOP in JavaScript. We'll spend the remaining chapters diving into the details. Lest you think we're going for a refreshing swim, JS OOP can be likened more to a mud bath. It's good for you, but you're going to get dirty.
Consider an application that manages different types of animals, such as dogs, cats, and birds. Each animal can eat, sleep, make sounds, etc., but they each do so in different ways.
Which of the concepts discussed in this chapter most closely describes the features of the scenario described above?
The features of the application described above most closely relate to the concept of polymorphism. Though each animal is of a different species, each has behaviors that are specific to that animal.
If you said that the application most closely relates to inheritance, consider yourself half right. The key difference here is that each animal eats, sleeps, or make sounds in different ways. Thus, the behaviors aren't being inherited.
Consider an application that uses and manipulates objects that represent automobiles. Each automobile has a make, model, year, and methods that provide the ability to start, drive, and park the vehicle. All automobiles share the same set of methods, but the make, model, and year will vary between automobiles.
Which of the concepts discussed in this chapter most closely describes the features of the scenario described above?
The features of the application described above most closely relate to the concept of encapsulation. Each automobile has different state but they all share the same behaviors.
Given the application described in the previous exercise, which items are part of an automobile's state? Which items provide its behavior?
The make, model, and year comprise the automobile's state. The start, drive, and park methods provide the automobile's behavior.
Consider an application that manages a collection of living things, including plants and animals. Plants include trees and flowers, while animals include mammals and birds.
Which of the concepts discussed in this chapter most closely describes the features of the scenario described above?
The features of the application described above most closely relate to the concept of inheritance. In this application, plants and animals would inherit from a type that includes living things, trees and flowers would inherit from a plants type, and mammals and birds would inherit from the animals. A plant is a living thing, as is an animal. Likewise, trees and flowers are plants, while mammals and birds are animals.