Many object-oriented programming (OOP) languages provide a way to define private properties and methods. This allows objects to encapsulate functionality and state information. This encapsulation leads to a clear distinction between the internal implementation and a clean external interface.
However, JavaScript does not have a native mechanism for declaring private properties and methods, in the same sense that other object oriented languages do. But, there are a few techniques that we can use to achieve the same results.
Convention-Based
The first technique we will consider is the use of convention-based naming to denote private properties and methods. The typical convention is to prefix names with an underscore to indicate they are private. This informs other users that the property or method is private, and shouldn’t be used.
Here is a simple example of a private property, private method, and public method in a class created with Dojo‘s declare
(using standard prototypal inheritance):
declare(null, {
_privateProperty: 3,
_privateMethod: function () {
return this._privateProperty;
},
publicMethod: function () {
return this._privateMethod();
}
});
Terminology
There are a couple of key JavaScripts concepts referenced in this post:
- Closure – A closure is a scope of variables (and arguments) of data and functions, that remains active through the references of other inner functions. There generally is a single closure for each module that may contain data and functions, and one can create closures to correspond to individual object instances as well.
- Prototypal Inheritance – JavaScript uses direct object-to-object (or prototype) inheritance to achieve the type of functionality sharing and inheritance usually achieved through classes in other languages. One can define functions on prototypes (typically through a class constructor like Dojo’s
declare
), to provide method-like functionality on object instances.
There are a few advantages to this approach:
- It fits well with JavaScript’s object-oriented paradigm.
- Private properties and functions can exist alongside public ones, attached to the prototype.
- There is no barrier to access between private and public methods and properties.
- Private methods can be defined on object prototypes, and easily inherited by other sub classes and object instances without any extra overhead.
However, convention-based private properties are not really private; they don’t actually prevent users from accessing these properties. This can be an advantage or a disadvantage. Often private methods or properties might actually end up being very useful to the users of an object. When we use convention-based access, users still retain the flexibility of ignoring the convention to override or directly call or access the private method or property. However, this type of access may not be advantageous for version upgrades. We will look at this concern in the next section.
Instance-Specific Closures
The next option for creating private members is to use instance-specific closures. A closure is a function scope within which we can create private functions and variables alongside functions that are publicly assigned and accessible outside the closure. Typically this is done in the constructor of the class, where the constructor function’s scope is used to contain the private functions, and public functions can be created prior to the new instance being accessible and any method calls being dispatched on it. Here is an example of using this type of closure to create a private property (as a variable), private method, and public method:
declare(null, {
constructor: function () {
var privateProperty = 3;
function privateFunction () {
return privateProperty;
}
this.publicMethod = function () {
return privateFunction();
};
}
});
There are several benefits and disadvantages of this approach as well. The first advantage is that it can be used to create truly private methods and state information, which really can not be accessed outside of the closure. Closures can also yield very clean code. When accessing private state values and the private functions, we don’t need to reference them through a property on `this`, they can be directly referenced. We also do not need to litter our code with underscores: the declaration indicates the scope without needing any extra conventions (although you could still use it). All this can produce cleaner, more readable, and less-error prone code.
However, there are a few issues with this approach to be aware of. First, closures are notorious for consuming more memory. With the typical convention-based (prototypal) declaration, a single set of methods can be shared across all instances. But in the case of closures, each instance allocates a new scope instance for the constructor, as well as new function and variable slots for each of the function and variables in the scope of the constructor, which can easily add up to a large amount of memory consumption if there are a high number of instances.
Second, methods declared in the traditional way, on the prototype of a class, cannot access the private functions or variables from the constructor scope. Any public method you wish to create, that will need access to these private functions, must be declared within the same constructor scope (you can see how we created the public method in the example above to be able to access the private function), and can’t be created through Dojo’s declare
.
Truly private methods may also present an inconvenience to the end user, who may really want to call your private function or override it. This is often cited as an issue with using closures. However, this reasoning deserves some caution, as well. From the perspective of looking at a single static version of some code, making otherwise private functions available can be alluring. But, one of the key reasons we denote functions as private is because they are part of our internal implementation, which we may anticipate or wish to change in future versions to improve the quality of our component. If we make these private methods accessible (through convention-based denotation of private), anyone who uses these methods creates a dependency on them, and future versions that eliminate or change the private methods can easily break the code with dependencies on the old version of these methods. If we are creating private methods for the purpose of protecting our internal implementation to facilitate future upgrades, using convention-based private members can defeat that very purpose. Whereas closure based private methods can keep methods truly private and help to ensure smoother future upgrades. Remember, it is much easier to switch a private method to a publicly accessible method in a future version than vice-versa. Once you have made something accessible, it can be very difficult to hide or change it without causing grief for your users.
Class-Specific Closures
Finally, there is third option available. Closures can also be used outside of class construction to create class-specific closures instead of instance-specific closures. In this case, we use a single closure to define our private functions, rather than generating new functions inside of the constructor. The closure scope may come from an immediately invoked function expression or, if an AMD modules is being defined, from the scope of the module’s factory function. Here is an example of using this option (within a define()
‘ed module):
define([...], function (...) {
var privateProperty = 3;
function privateFunction () {
return privateProperty;
}
return declare(null, {
publicMethod: function () {
return privateFunction();
}
}
});
One of the key differences in this approach should be immediately made known: since there is only a single instance of the closure, we can not have instance-specific private state values. The privateProperty
variable declared here is shared by all instances.
There are some important advantages with this approach which address some of the key problems with the instance-specific closures. First, we no longer have any instance-specific memory overhead. Just as with convention-based private methods, instances only retain the memory of the object itself, providing much more efficient memory usage.
Second, methods declared in the traditional way, on the prototype of the class, have full access to all the private functions (and class-specific variables), one does not need to alter the declaration of these public methods to give them access to private methods.
You may be hesitant about using functions that don’t sit on the instances as methods. However, remember that the instance context is essentially just another argument that is passed to a function with some nice syntax. We can still easily give these functions access to the instance and all its (public) properties and methods.
Combining Techniques
Because class-specific closures allow methods on the prototype to access the private methods and variables, this gives us the option to be able to combine techniques. For example, we could easily use a convention-based private property in conjunction with a class-specific closured function, allowing us to have a truly private function, while still using instance-specific data without any instance-specific extra memory overhead:
define([...], function (...) {
function privateFunction (instance) {
return instance._privateProperty;
}
return declare(null, {
_privateProperty: 3,
publicMethod: function () {
return privateFunction(this);
}
}
});
Looking Ahead: WeakMaps
One of the exciting new features that has been proposed for EcmaScript 6 is the WeakMap interface. A WeakMap is an object that supports objects as keys. By using a WeakMap, we can actually create a class-specific closure on a WeakMap, yet use instances as the keys to define instance-specific data. Here is how we could upgrade the previous example to use WeakMaps:
define([...], function (...) {
var privatePropertyMap = new WeakMap();
function privateFunction (instance) {
// set a private property value for this instance
privatePropertyMap.set(instance, someValue);
// get a private property value
return privatePropertyMap.get(instance);
}
return declare(null, {
publicMethod: function () {
// we can access the private property values here as well
privatePropertyMap.get(instance);
return privateFunction(this);
}
}
});
WeakMaps will automatically remove entries when an object key is garbage collected, so private property values are cleaned up as a natural part of the garbage collection process. WeakMaps are currently only available in Firefox and Chrome with the experimental features enabled.
Comparison of Features
So which option should you use? Each of these different approaches are suited for different situations. To help summarize, let’s compare the benefits and capabilities of these different approaches:
Convention-Based | Instance-Specific Closures | Class-Specific Closures | |
---|---|---|---|
Per-instance memory overhead | No | Yes | No |
Truly private | No | Yes | Yes |
Accessible from standard prototypal methods | Yes | No | Yes |
Permits instance-specific state | Yes | Yes | No |
Enables direct referencing of private functions | No | Yes | Yes |
As for general advice for which technique to use, I’ll conclude with a few suggestions. Each of these approaches have appropriate use cases, here some final guidelines:
- Avoid instance-specific closures for classes for which you expect to have a high number of instances (like thousands).
- Be very careful about what you are communicating with convention-based private methods and properties. If you are trying to privatize these for the sake of future implementation upgrades, the underscore convention may be insufficient. If you do not foresee any changes, in some cases it may be better to simply make your methods public and document when and when to use them.
- Class-specific private functions can often provide the best of both worlds, but again remember that you can’t use them to store instance-specific private state information.
- Once WeakMaps are widely available, you can utilize these for truly private instance-specific data.
Often using a combination of these tools can yield the greatest flexibility, balancing of performance, and maintainability of your code.