How to Safely Inherit Private Data in JavaScript
Often, people use underscores to denote that a property or method should be considered private. That's a bad idea.
Why Underscores Are a Bad Idea
Underscores do not guarantee data privacy, and there are a couple important problems that they create:
- Newbies don't know what underscores mean, so they ignore them.
- Advanced users think they know what they're doing, so the underscores don't apply to them.
- Implementation details may change, which may break the code the user code that used the underscore properties.
These are problems because encapsulation is an important feature of object-oriented design. Objects exist to solve a particular problem. Private methods may solve problems which are only related as an implementation detail. Implementation details are more likely to change than public interfaces, so code that relies on implementation details may break when implementation details change.
Exposing only your public interface hides implementation details which may change, which guides users to rely on supported features, rather than unsupported implementation details.
Use Functional Inheritance for True Data Privacy
Functional inheritance can be employed to inherit private data.
Functional inheritance is the process of inheriting features by applying an object augmenting function to an object instance. The function supplies a closure scope, which has the effect of hiding private data inside the function's closure.
Douglas Crockford coined the term in "JavaScript: The Good Parts". In Crockford's example, a child factory knows how to instantiate an object from an existing base factory, which has the effect of creating an inheritance hierarchy. However, that's a bad idea. We should always favor object composition over class inheritance.
You can create and compose functional mixins by modifying the pattern slightly to take the base object as a parameter.
The functional mixin enhances a supplied object instance. The function's closure scope may contain private methods and data. You may then expose privileged methods inside that function. It works like this:
const withFlying = instance => {
let canFly = true; // private data
let isFlying = false;
// Privileged method
instance.fly = () => {
isFlying = canFly ? true : isFlying;
return instance;
};
// Privileged method
instance.land = () => {
isFlying = false;
return instance;
}
// Privileged method
instance.getFlightStatus = () => isFlying ? 'Flying' : 'Not flying';
return instance;
};
// Create a new object and mix in flight capability:
const bird = withFlying({});
console.log(bird.fly().getFlightStatus()); // true
bird.land();
console.log(bird.getFlightStatus()); // false
Functional mixins can be composed together using standard function composition with any other base object and any other set of features. First, you'll need a compose function. You can use compose()
from Lodash, Ramda, or any functional programming library that provides a standard compose function -- or just write your own:
// Function composition: Function applied to the result of another function application, e.g., f(g(x))
// compose(...fns: [...Function]) => Function
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
Now you can compose any number of mixins together using standard function composition:
// This function returns a function which can be used
// as a functional mixin.
// `text` here is private data that determines the sound
// `quack()` will log to the console.
const withQuacking = text => instance => {
// Privileged method
instance.quack = () => console.log(text);
return instance;
};
// Compose mixins:
// ('Quack!' is private data)
const createDuck = compose(withFlying, withQuacking('Quack!'));
const malard = createDuck({});
console.log(malard.fly().getFlightStatus()); // Flying
malard.quack(); // "Quack!"
For more versatile ways to compose factory functions using a variety of inheritance techniques, see the Stamp Specification.
References:
- Douglas Crockford, "JavaScript: The Good Parts"
- Eric Elliott, "Three Different Kinds of Prototypal Inheritance"
- Eric Elliott, Vasyl Boroviak, et al., Stamp Specification