Intro to Mod 5: Object-Oriented Programming (OOP) — 5 minutes
Object-Oriented Programming is a style of programming (a "paradigm") that uses objects to manage state (data) and behavior in an application. While OOP does let us do some new things, more than anything, it helps us write better, more organized code.
It can be defined by its 4 pillars:
Encapsulation - bundling data and methods into a single unit while protecting the data
Abstraction - hiding complexity through functions and prototypes
Inheritance - sharing behavior between objects
Polymorphism - similar objects can be used interchangeably
Throughout this module, we will be learning about these four pillars and how we implement them in JavaScript using the class syntax.
Encapsulation — 15 minutes
In functional programming, we separate data from the functions that act on them. We achieve consistency & predictability through pure functions.
// Functional Programming separates data from functionality
const friends = ['ahmad', 'brandon', 'carmen'];
const addFriend = (friends, newFriend) => {
if (typeof newFriend !== 'string') return friends;
return [...friends, newFriend]; // keep it pure, make a new list
}
const newFriends = addFriend(friends, 'daniel');
In OOP, we store data in objects and give those objects methods to manipulate their own data. This is called encapsulation.
// Object Oriented Programming encapsulates data with functionality
const friendsManager = {
friends: [],
addFriend(newFriend) {
this.friends.push(newFriend);
// `this` refers to the "owner" of the method.
}
}
friendsManager.addFriend('ahmad');
friendsManager.addFriend('brandon');
friendsManager.addFriend('carmen');
// Here, friendsManager invokes addFriend so this === friendsManager
console.log(friendsManager.friends)
Side Note on this:
When used as method of an object, this refers to the object that invokes the method.
Q: How does the friendsManager object demonstrate encapsulation compared to the first example? How does this enable encapsulation?
The friendsManager object stores the friends array inside alongside the addFriend method.
When friendsManager.addFriend() is invoked, the addFriend method uses this to manipulate the friendsManager's own friends array.
Designing A Consistent and Predictable Interface — 5 minutes
Consistency and predictability are major goals in software engineering. This is what motivates us to write pure functions in functional programming. It is just as important in OOP.
Consider the friendsManager example again. Notice that we've added a guard clause to ensure that only strings are added as friends.
const friendsManager = {
friends: [],
addFriend(newFriend) {
if (typeof newFriend !== 'string') return;
this.friends.push(newFriend);
}
}
// What about this is NOT consistent or predictable?
friendsManager.addFriend('daniel');
friendsManager.addFriend(true);
friendsManager.friends.push('emmaneul');
friendsManager.friends.push(42);
Q: What about the last two lines in the example are NOT consistent or predictable?
You can modify the friendsManager.friends array either through the addFriend() method or by directly mutating the friends array. When modifying the array directly, there are no safeguards.
Factory Functions, Privacy, and Closures — 30 minutes
A core tenet of encapsulation in OOP is to hide values like friends from being directly accessed.
Let's see how we can do this using closure. A closure is when an "inner function" maintains references to variables in its surrounding scope (an "outer function").
Here's how we use closure to protect the friends array:
const makeFriendsManager = () => {
// this variable is in the "outer" function
// and referenced in addFriend and getFriends
const friends = [];
const friendsManager = {
addFriend(newFriend) {
if (typeof newFriend !== 'string') return;
friends.push(newFriend);
}
getFriends() {
return [...friends];
},
}
return friendsManager;
}
const bensFriendsManager = makeFriendsManager();
bensFriendsManager.addFriend('zo')
bensFriendsManager.addFriend('motun')
console.log(bensFriendsManager.friends) // undefined
console.log(bensFriendsManager.getFriends()) // ['zo', 'motun']
We put the object in a function that returns the object.
We move the friends array outside of the object so that the returned object doesn't have direct access to it.
The returned object still has the addFriend method, but it can't reference this.friends anymore. Instead it references the friends variable from the surrounding scope. this is closure.
The returned object has a new getFriends method that returns a copy of the friends array to avoid sharing the reference.
The friends array is fully private but we can still interact with it using the object's setter/getter methods.
The cool thing about closures is that each time we invoke this function, we will create a new friends array and a new object with methods that reference that specific instance of the friends array.
Q: In the example above, identify the "outer" function and the inner function involved in creating a closure
makeFriendsManager is the outer function and both addFriend and getFriends form a closure around the variable friends.
Q: How can we modify the example above to be able to create a new friend manager with a starting set of friends as an argument?
We've seen closures before in non-object-oriented contexts. Consider the functions below. Which of them creates a closure? How?
const sayWordsLoudly = (words) => {
words.forEach((word) => console.log(`${word}!!!`));
}
const addRandomToNumbers = (nums) => {
const randomNum = Math.random();
return nums.map((num) => num + randomNum);
}
const addAmountToNumbers = (nums, amount) => {
return nums.map((num) => num + amount);
}
// This is an "IIFE" (Immediately Invoked Function Expression)
const getId = ((id = 1) => () => id++)();
Answer
The first function DOES NOT create a closure. Even though there is an inner arrow function defined, that function doesn't reference variables in the scope outside of it
The second function DOES create a closure because the inner arrow function passed to nums.map references the randomNum variable in the scope outside of it.
The third function DOES create a closure for the same reason as the function above. Referencing the parameter amount is the same.
The fourth function DOES create a closure because we have an outer arrow function returning an inner arrow function. The inner arrow function () => id++ references the id parameter in the outer arrow function.
Challenge
Below is a counter object. The problem is that the counter.value property is not private — it can be directly mutated. Your challenge is to create a function makeCounter that will protect the value of the counter while still allowing us to increment(), decrement(), and get the current value of the counter.
As a bonus, make the function accept an argument startingValue which sets the starting value of the counter. If no value is provided, start at 0. Then make multiple counters, each starting at a different value.