JavaScript Objects and Private Data
In this brief article, I will attempt to illustrate differences in JavaScript’s treatment of private data using 2 of its object creation patterns. In doing so, I hope to expose a few tradeoffs between the 2 patterns when encapsulated private data is desired.
First, we will briefly examine the 2 object creation patterns in question before introducing how each can address private variables. For an excellent, more detailed examination of these patterns check out Fundamental Object Design Patterns in JavaScript.
Pseudo Classical Pattern
Using the pseudo-classical pattern of object creation, we (1) instantiate objects with unique property values using a constructor function, and (2) define shared behavior on the constructor’s prototype object. It is critical to understand that each object created from a constructor has a __proto__
property which references the prototype
object of the constructor function. This allows the delegation of behaviors across a hierarchy of chained prototype objects.
When we invoke describe
on our Car
object mySedan
, our object checks to see if it was defined with a method named “describe” (it wasn’t). Our object then checks its __proto__
object where the method is found, and executed.
Objects Linking to Other Objects (OOLO)
Using this pattern, we fully embrace JavaScript’s prototypal inheritance and distance ourselves further from class-based models of object creation. Using OOLO, we define an object that will be used as a “blueprint” prototype for other objects. Using Object.create()
, we create a new object whose __proto__
object references the object passed to create
.
In order to provide our created object with its own unique property values, we define an init
function which is invoked separately from the instantiation of the object. This adds an additional step to the creation of unique objects compared to the pseudo-classical pattern’s usage of constructor functions with the new
operator.
Below, we see a few objects created from the Car
“mold” in ex2, the properties of these objects, and whether the instances can be determined to have been created from Car
:
Introducing private data to these patterns:
Using our Car
example, let’s add a private variable speed
whose value can only be updated using the method accelerate
.
- The Pseudo-classical pattern:
My first inclination was to declare a local variable speed
whose value be tracked by way of a closure, and our accelerate
method:
This accomplished our goal of creating a private variable speed
. The expense of this solution, however, is that all objects created from Car
hold copies of our accelerate
, and viewSpeed
methods. This is not exactly a DRY solution and could. With the addition of more private variables, additional functions will be required which, in this example, would be owned by every instance of the Car
object. Perhaps we can write these methods on the constructor’s prototype to avoid duplication of code:
Here, we discover that our local variable speed
is out of scope within the prototype methods. This issue can be remedied by adding setter and getter methods to the constructor, which reintroduces methods into the constructor.
Adding a setter and getter for speed
to the constructor adds unnecessary code rendering our accelerate
method written on the prototype redundant since the value of speed
can be reassigned directly using the setSpeed
method:
In the last 2 examples, we moved our accelerate
to the constructor’s prototype, but then had to reintroduce code (the setters and getters) in the constructor to access our speed
variable. This does not seem to be the DRY solution we’re looking for.
In another article on JS object creation patterns, John Dugan explores the “Durable Constructor Pattern” and how it may provide an object with private data accessible via methods on the object. This pattern is similar to an object factory in that the constructor is invoked without the new
operator, and returns a new object. This pattern makes use of a closure between the methods defined on the new object, and the private variable. One upside to this approach is its legibility — the private variable, and the methods with access to it are nicely bundled. The downside is that since we do not invoke the constructor with the new
operator, there is no relationship between the object returned and the constructor function, ie, the prototype object of Car
is unavailable to Car
objects:
Takeaways from the pseudo-classical pattern:
It seems that using the pseudo classical pattern of object creation often requires that our objects all hold copies of functions in order to interact with private variables. Put another way, the methods that have access to an object’s private data need to exist in the same function scope. As a result, we wind up with duplicate code in our object’s instances. If our application only requires a few instances of the Car
object, this duplication is not very costly, but in an application with millions of Car
objects, a DRYer solution would be better. Compared with object creation patterns that leverage prototypal inheritance to define shared behaviors in a single location, it is redundant for every instance of an object type to carry a copy of a shared behavior.
- The OLOO (Objects Linked to Other Objects) Pattern:
Using this pattern, we use Object.create(obj)
to create an object using obj
as its prototype.
So, we know that our Car
prototype must return an object to be used as our instances’ prototype. All shared behavior can be written on the Car
prototype, and unique object property values can be initialized using an init
method.
Here, we have written our accelerate
method on the Car
(prototype) object. We somehow need to be able to create a private scope that tracks the speed of our car instances…
~ Enter the IIFE ~
Immediately Invoked Function Expressions are functions like any other function, except that they are invoked immediately after definition. An IFFE is a function expression appended with ()
to immediately invoke it.
IFFEs are often used to create a private scope since, as functions, they create a new scope. If we think back to the requirements of our OOLOCar
object creation, we stated that it needs to return an object to be used as our instances’ prototype. If we create an IFFE that returns our prototype object, we can declare other “private” variables scoped within the IFFE, but outside of the returned prototype object. Using closure, our prototype’s methods retain access to the variables in scope at the time of function declaration. Presumably, this means we could create a local variable speed
accessible via closure by our prototype’s methods:
Unfortunately, as the above example illustrates, our 2 car objects retain access to the same speed
variable. As such, each object does not track a unique speed
value since in this example, speed
operates as something of a class variable which can be updated by any instance of the “class”.
One solution to this dilemma is to leverage this “class variable” to hold the private data for all instances of Car
. In this example, each instance of Car
will need a unique id
property used to lookup its associated private data. Therefore, we will use a privateData
lookup object that will hold the speed property (among other private properties) of a given instance:
Takeaways from this implementation of the OOLO Pattern:
To me, the biggest benefit of this solution over our pseudo-classical pattern solution is that we have a working Car
prototype which eliminates the need for every instance to hold a copy of shared Car
behaviors (like the accelerate
function). The drawback here is that the private data for every instance of Car
is centralized within the privateData
object. Additionally, we’ve added more “setup” code during object instantiation: we need to create and assign an id
for every car, and map each instance to an object within privateData.
I hope this basic examination private data in 2 different object creation patterns has helped you develop a basic understanding of these patterns as well as an appreciation for JavaScript’s complexity in regards to private data!
Please comment with any feedback about my examples, or examples of your own!