2. Data Hiding through Closures
Follow along with code examples here!
Table of Contents:
Key Concepts
Data hiding is the act of limiting access to an interface's data in order to prevent users of the interface from introducing invalid data that would break the program's logic (e.g. adding a
nullvalue to a list of strings). Data hiding is a key outcome of effective encapsulation.A closure is created when an "inner function" references a variable declared in the surround scope. It can be used to hide data in the factory function pattern.
Intro: Closures
Consider this function addSuffixToEachWord below. It takes in an array of words and returns a new array with a given suffix added to the end of each word.
Without you knowing it, this example creates something called a closure
Closures are created when an "inner function" maintains references to variables in its surrounding scope (an "outer function").
Q: In this example, what is the "outer" function and what is the "inner" function? Which variable from the outer function is referenced by the inner function?
The outer function is addSuffixToEachWord and the inner function is the anonymous callback function (word) => word + suffix.
The inner function references the parameter suffix from the outer function's scope.
Why are Closures Important? A Closure Counter
The applications of closures are really interesting. Consider this classic example:
Here's how this works:
The outer function
makeCounterdeclares a variablecountthat is referenced by the inner functioncounter.Due to the closure, the
myCounterfunction can still referencecount, even after the outer function finishes executing.Aside from the
myCounterfunction, we have no way to directly reference thecountvariable.
Another cool thing about closures is that each time the outer function is invoked, a new count variable and a new inner function are created, each with their own closure:
Let's see how we can leverage this in object-oriented programming to support encapsulation.
Data Hiding Prevents Invalid Data
Remember, the goal of any programming paradigm is to write code that will behave consistently and predictably.
Consider the friendsManager example again. Notice that we've added a guard clause to ensure that only strings are added as friends. We want to ensure that we don't introduce any invalid data into our list of friends.
Q: What about this example is NOT consistent or predictable? What is printed by the last statement?
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.
The
friendsarray can be directly modified from the outside, which leads to uncontrolled additions likefriendsManager.friends.push(42). This results in an array that might have invalid data types, such as numbers and booleans mixed with strings, violating the intended structure of the friends list.This unpredictability leads to bugs and confusion, as the integrity of the data inside
friendsManagercannot be guaranteed.
The final statement shows that 'emmanuel' and 42 have been added to the list.
Despite our attempts to prevent invalid data, the friends array is directly accessible through the friendsManager.friends property. As a result, we can't guarantee that our rules for adding new friends are enforced.
Factory Functions and Closures Let Us Hide Data
Data hiding is the act of limiting access to an interface's data in order to prevent users of the interface from introducing invalid data that would break the program's logic (e.g. adding a null value to a list of strings). Data hiding is a key outcome of effective encapsulation.
In this case, we want to hide the friends array so that it can't be directly accessed.
In JavaScript, we can modify our factory function to use closure to do this:
Here's how this works:
We declare the
friendsarray as its own variable, outside offriendsManager. It can no longer be accessed directly via the object.The methods
addFriendandprintFriendsreference thefriendsarray in the outer scope, forming a closure.Invoking
makeFriendsManager()returns thebensFriendsobject whose methodsaddFriend()andprintFriends()maintain their reference to thefriendsvariable through the closure.The internal
friendsarray is now inaccessible except through the interfaceaddFriend()andprintFriends().The validation guard clause in
addFriend()now prevents invalid data to enter the system.
Because addFriend and printFriends were declared within the same scope as friends, they can continue using their reference to friends even after leaving that scope.
Always Return Copies of Pass-By-Reference Values
Suppose we wanted to provide a way to access the list of friends, we would add a "getter" method like getFriends:
To ensure friends remains hidden, the getFriends method returns a copy of the friends array to avoid sharing the array reference.
Whenever you are dealing with a pass-by-reference data type (arrays and objects), always make sure to return a copy of it!
Every Instance Creates a New Closure
Remember, 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?
Quiz!
We've seen closures before in non-object-oriented contexts. Consider the functions below. Which of them creates a closure? How?
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.mapreferences therandomNumvariable in the scope outside of it.The third function DOES create a closure for the same reason as the function above. Referencing the parameter
amountis 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 theidparameter 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.
Last updated