Mimicking Object-Oriented Programming with JavaScript Factory Functions: Encapsulation
Say you want to model a user with JavaScript. After much deliberation, you come up with a simple specification:
- A user has a username, a password, and a bio.
- A user has two methods. One returns a greeting, the other checks if an entered password is correct.
- A user has getters and setters for username and bio. You don’t want someone to directly modify a user’s data.
- A user’s password will be private and inaccessible.
Let’s take a look at the various ways JavaScript allows us to create the proposed user object.
Plain Old JavaScript Objects
const user = Object.freeze({
_username: 'Tracy',
_password: 'secret123',
_bio: 'Loves JavaScript', sayHello() {
return `Hi, my name is ${this._username}`;
}, checkPassword(pwToCheck) {
return pwToCheck === this._password;
}, get username() { return this._username; },
set username(newUsername) { this._username = newUserName; },
get bio() { return this._bio; },
set bio(newBio) { this._bio = newBio; }
});console.log(user._username); // Tracy
console.log(user._password); // secret123
A Plain Old JavaScript Object (POJO) is the simplest of approaches. It allows us to fulfill requirements 1 and 2, but does not stop us from directly viewing all three user properties.
An underscore is commonly used to signal privacy, which may be enough for other developers reading your code, but will in no way aid in security.
We also have to manually create the object each time we want a new user, which is impractical.
Constructor Functions
Constructor functions allow us to achieve true encapsulation through scope closures.
function User({ username, password, bio }) {
this.getUsername = () => username;
this.setUsername = newUsername => username = newUsername; this.getBio = () => bio;
this.setBio = newBio => bio = newBio; this.sayHello = () => `Hi, my name is ${this.username};
this.checkPassword = pwToCheck => pwToCheck === password;
}const user = Object.freeze(new User({
username: 'Tracy',
password: 'secret123',
bio: 'Loves JavaScript',
}));console.log(user.username); // undefined
console.log(user.password); // undefined
This works exactly how we want. As long as we do not bind the properties to “this”, we cannot access them, allowing the designer to decide what is public.
Constructor functions are not without issue. The excessive use of “this” is cluttering up the code with a ton of error-prone boiler-plate code. If we forget the “new” keyword, we are going to attach every method to the global object.
ES6 Classes
ES6 classes were an attempt to get rid of all of the boiler-plate code of constructor functions and resemble something similar to Java classes. In some areas they excel, especially prototypical inheritance. But how do they do with privacy?
class User {
constructor({ username, password, bio }) {
this._username = username;
this._password = password;
this._bio = bio;
} sayHello() {
return `Hi, my name is ${this._username}`;
} checkPassword(pwToCheck) {
return pwToCheck === this._password;
} get username() { return this._username; }
set username(newUsername) { this._username = newUsername; }
get bio() { return this._bio; }
set bio(newBio) { this.bio = newBio; }
}const user = Object.freeze(new User({
username: 'Tracy',
password: 'secret123',
bio: 'Loves JavaScript',
}));console.log(user._username) // Tracy
console.log(user._password) // secret123
Well, they may be a clean, but their ability to encapsulate is nonexistent.
Factory Functions
Factory functions are becoming increasingly popular with the recent excitement in functional programming. They are simply functions that return an object defining a public interface. The functions on the returned object are able to update the object’s state, without exposing private data, through closures.
function User({ username, password, bio }) {
const sayHello = () => `Hi, my name is ${username}`; const checkPassword = pwToCheck => pwToCheck === password; return Object.freeze({
sayHello,
checkPassword,
get username() { return username; },
set username(newUsername) { username = newUsername; },
get bio() { return bio; },
set bio(newBio) { bio = newBio; },
});
}const user = User({
username: 'Tracy',
password: 'secret123',
bio: 'Loves JavaScript',
});console.log(user.username); // undefined
console.log(user.password); // undefined
Like constructor functions, factory functions allow for true encapsulation, but do so without any use of “this” or “new”, and are much less error-prone as a result. We may also specify Object.freeze() inside the factory function, allowing the designer to control immutability, not the instantiator.
What to choose? Not “this”
Go with factory functions. They’re simple and powerful, allow for a more functional programming style, and can mimic object-oriented principals well. If you’re a React developer, this is a pattern you will soon know well if you plan on using hooks.