Mimicking Object-Oriented Programming with JavaScript Factory Functions: Encapsulation

Tracy French
3 min readJan 21, 2019

--

Say you want to model a user with JavaScript. After much deliberation, you come up with a simple specification:

  1. A user has a username, a password, and a bio.
  2. A user has two methods. One returns a greeting, the other checks if an entered password is correct.
  3. A user has getters and setters for username and bio. You don’t want someone to directly modify a user’s data.
  4. 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.

--

--