Zach Caceres
Javascript does not have the typical 'private' and 'public' specifiers of more traditional object oriented languages like C# or Java. However, you can achieve the same effect through the clever application of Javascript's function-level scoping. The Revealing Module pattern is a design pattern for Javascript applications that elegantly solves this problem.
The central principle of the Revealing Module pattern is that all functionality and variables should be hidden unless deliberately exposed.
Let's imagine we have a music application where a musicPlayer.js file handles much of our user's experience. We need to access some methods, but shouldn't be able to mess with other methods or variables.
Let's first see how Javascript's function-level scope can help us create public and private methods.
We can move all functionality inside a function's scope. Then we return an object with the functions that we'd like accessible in other files.
// musicPlayerModule.js
var musicPlayer = function () {
// Let's make sure no one can directly access our songList
var songList = ['California Girls', 'California Dreaming', 'Hotel California'];
// We'll expose all these functions to the user
function play () {
console.log('Im playing the next song!');
}
function pause () {
console.log('Im paused!');
}
function addTrackToMusicQueue (track) {
songList.push(track);
console.log('I added a song');
}
function showNextTrack () {
console.log('My next track is', songList[0]);
}
// Let's hide this function
function loadSong() {
filesystem.loadNextSong();
}
return {
playMusic: play,
pauseMusic: pause,
showNextTrack: showNextTrack,
addTrack: addTrackToMusicQueue
}
}
const musicModule = musicPlayer(); // invoke our musicPlayer to return it's object (module)
musicModule.playMusic(); // 'Im playing the next song!'
musicModule.pauseMusic(); // 'I'm paused!'
musicModule.showNextTrack(); // 'The next track is California Girls'
// Things we can't access...
musicModule.loadSong(); // error: not a function
musicModule.songList.push('White Rabbit'); // undefined error
Now we can access all the methods that we need on our musicModule
object. But we can't fiddle with our songList
or access the loadSong()
method. These are both private.
This works fine. But there's one big problem.
We're using musicPlayer
as a namespace to hold our functions. But wait, our musicPlayer
is a function that's still exposed to the global scope!
Someone could come along and invoke it again, creating a new musicPlayer
. Then we would have multiple instances of musicPlayer
floating around, polluting our environment and causing all sorts of confusion.
The best way to avoid exposing your top-level function to the global scope is to wrap everything in an IFFE. An IIFE is an immediately-invoked function expression. That's a mouthful. It just means that we call (invoke) this function as soon as the file is run (immediately).
Since our function has no name, we call it an expression. Since it has no name, it can never be invoked elsewhere.
Here's how that looks:
var musicModule = (function () {
// Let's make sure no one can directly access our songList
var songList = ['California Girls', 'California Dreaming', 'Hotel California'];
// We'll expose all these functions to the user
function play () {
console.log('Im playing the next song!');
}
function pause () {
console.log('Im paused!');
}
function addTrackToMusicQueue (track) {
songList.push(track);
console.log('I added a song');
}
function showNextTrack () {
console.log('My next track is', songList[0]);
}
// Let's hide this function
function loadSong() {
filesystem.loadNextSong();
}
return {
playMusic: play,
pauseMusic: pause,
showUpNext: showNextTrack,
addTrack: addTrackToMusicQueue
}
})(); // our IIFE function (surrounded with parens) is invoked here
musicModule.playMusic(); // 'Im playing the next song!'
musicModule.pauseMusic(); // 'I'm paused!'
musicModule.showUpNext(); // 'The next track is California Girls'
musicModule.loadSong(); // error: not a function
musicModule.songList.push('White Rabbit'); // undefined
Our function-level scope still keeps our methods and variables public and private based on whether we expose them in the return object.
But this time, we avoid the risk that we can invoke our module elsewhere in the code.
In other files we can now use musicModule
's' functionality – a well-encapsulated module in our global scope!
The Revealing Module Pattern is NOT outdated, but the singleton pattern (IIFE) shown in this gist is. As I noted in a previous comment, the instantiable Revealing Module Pattern has always been far more useful...
This remains true today, and the pattern works great inside an ES6 module. I have used this pattern multiple times in the past year.
An ES6 module is a singleton, so is an replacement for an IIFE. However because it is a singleton, you need to export a instantiable function if you need multiple, independent instances of the object, each with its own internal state.
React hooks are essentially like the Revealing Module Pattern. You define state and methods inside a function, and return only the specific values and methods desired. Every time you use a hook, a new instance is created. The popularity of hooks illustrates how useful and versatile this pattern is.
However not everyone uses React, and even if you do, not every object should be written as a hook. Hooks can only be used within a React component, but sometimes you want a helper that can be instantiated and used outside a component, and that can continue to exist beyond the lifecycle of a component.
For example, a module may perform asynchronous actions that complete after a component unmounted, and then performs other global actions. A module is good way to do this.
It is also useful to combine an instantiable object with a singleton ES6 module. Common state can exist at the ES6 module level, with instance state maintained inside the Revealing Module instance. This provides a way for one instance to share state with other instances, and for instances to interact with each other.
For example, if one instance loads data useful to all instances, this can be put in the shared scope. An internal instance method can also be placed in the shared scope by one instance while an action is in progress, allowing another instance to interact with with the first instance if a conflicting or superceding action is triggered. Or the common scope could be used to create a 'registry' of all active instances, which allows any one instance to call methods in any other, or all at once.
These are just a few examples of how this pattern is still useful today. It leverages the power of JavaScript closures, so is likely to remain useful for a long time.