- Modules vs Classes — How to Manage Privacy in JS
- Classes and Objects
- Module Pattern
- Closures in JS and Why It Matters
- Ilê Caian ・ Feb 6 ’20 ・ 4 min read
- Wrap up
- References
- JavaScript Best Practices — Classes and Modules
- Class Methods Should Either Reference this Or Be Made Into a Static Method
- Use ES6 Modules Import and Export Over Non-Standard Module Systems
- Do Not Use Wildcard Imports
- Do Not Export Directly From an Import
- Conclusion
Modules vs Classes — How to Manage Privacy in JS
The Object-Oriented Paradigm made a huge shake in the way developers think and write code even if you don’t like it or don’t like its premises. This not so new paradigm of encapsulating data and procedures in form of attributes and methods influenced a lot of recent languages even if some of them are not using it as its main paradigm. Languages like: C++, Java, Python, and even JavaScript are considered and declared languages that implement the Object-Oriented Paradigm (OOP). As will be discussed, JavaScript has its own way of dealing with Objects with some specificities. But first, there is a starting point that should be discussed: there’s one essential fact in JavaScript that goes in the wrong direction of OOP: non-existence of encapsulation. There’s a repo with some tests too! Check it out!
Classes and Objects
Classes are definitions of data-types: what data they will store/hide and how it should behave. An instance of one Class will be able to execute functions as methods and store data as attributes. Those instances are the so-called Objects that lives inside the runtime execution of a program. One of the important features in OOP is the fact that the Objects should have the ability to encapsulate(hide) its data. That means that if someone tries accessing some information from the Object, it should not be able to do it if the Class says so. Consider the following example: Let’s say that Elon Musk 🧑💼 created an awesome Trash Can that can perform 3 simple tasks:
- Throw one ‘junk’ item into the Trash Can
- Clean all items inside the Trash Can, all at once
- One button that shows at the display if the Trash Can is fully empty or not
The interface for that TrashCan is something like:
TrashCan throwAway(item); clean(); isEmpty(); >
As JavaScript has the class keyword, it’s possible to consider one implementation of this Trash as the following
class TrashCan constructor() this.items = []; > throwAway(item) this.items = [. this.items, item]; > clean() this.items = []; > isEmpty() return this.items.length === 0; > > var elonTrashCan = new TrashCan();
Now the elonTrashCan is empty and ready to start doing its work. But what happens with the execution of
elonTrashCan.throwAway('paper ball'); elonTrashCan.throwAway('empty Starbucks cup of coffee'); elonTrashCan.throwAway('empty package of Cookies'); elonTrashCan.clean(); elonTrashCan.items = ['SpaceX secret project']; console.log(elonTrashCan.isEmpty()); // --> .
- Elon Musk 🧑💼 will be mad at us that we broke his Trash Can
- elonTrashCan.isEmpty() will return false , because we defined elonTrashCan.items with 1 item inside
- elonTrashCan.items is not accessible, so the elonTrashCan.isEmpty() call will returns true
The answer is option 2. It is possible to access items inside the Object instance even without explicit items declaration outside constructor .
Using that example and considering an ideal Object-Oriented language implementation, the execution of elonTrashCan.items should result in an error of the program trying to access a private attribute. In JavaScript, these calls are possible, valid ones, and results in no error.
So, isn’t it possible to create Privacy in JavaScript? Is there a way of hiding data from outside the Object and exposing just public data?
Module Pattern
The good news is there’s one behavior in JavaScript that provides something related to privacy: Closures. There’s this post written about Closures in case of interest
Closures in JS and Why It Matters
Ilê Caian ・ Feb 6 ’20 ・ 4 min read
Using Closures for hiding variables and functions is a good approach for encapsulate data inside one instance and just expose the desired interface of it.
But how do actually this works?
Let’s create the same Elon Musk 🧑💼 TrashCan Object writing a Function and returning just the public interface of it as the following code
const TrashCan = () => let items = []; const throwAway = item => items = [. items, item]; > const clean = () => items = []; > const isEmpty = () => return items.length === 0; > return throwAway, clean, isEmpty, > > var elonTrashCan = TrashCan();
And for the new elonTrashCan let’s try to execute the same code as above
elonTrashCan.throwAway('paper ball'); elonTrashCan.throwAway('empty Starbucks cup of coffee'); elonTrashCan.throwAway('empty package of Cookies'); elonTrashCan.clean(); elonTrashCan.items = ['SpaceX secret project']; console.log(elonTrashCan.isEmpty()); // --> .
- Elon Musk 🧑💼 will be even madder at us that we broke his second Trash Can
- elonTrashCan.isEmpty() will return false , because we again defined elonTrashCan.items with 1 item inside
- elonTrashCan.items is not accessible, so the elonTrashCan.isEmpty() call will returns true
Actually something very strange happens:
- elonTrashCan.isEmpty() returns false because our internal items are empty
- elonTrashCan.items has 1 item in it
Using this approach, it is possible to ‘limit’ the outside world on accessing just the desired interface and has hidden content inside it. On the other hand, JavaScript lets the definition of new properties in runtime execution, even with the same names used by its Closure.
The code inside of the Closure will not depend on those new properties, as the original ones are stored inside that Closure, unaccessible. And, the original mission is now accomplished: Privacy. The Module Pattern is viable for attributes and can be used for hiding methods too.
For the side-effect with the creation of new properties, it’s hardly suggested not to change the original interface and even make some tests before using those properties like
if(typeof elonTrashCan.items === 'undefined') console.log('No exposed items!') // --> No exposed items! >
Wrap up
After some discussion about the Object-Oriented Paradigm and the Class implementation of JavaScript, maybe the JS Class is not the best choice for creating Objects with private data if you are not using a transpiler like Babel.
Using Closures and the Module Pattern it is possible to accomplish Privacy in JavaScript in a simple and reusable way. If the class implementation is inevitable, consider using a transpiler or a more robust approach with some use of the Module Pattern. The use of transpilers is hardly incouraged!
Even with some apparent losses like inheritance , there are still valid ways of implementing that benefits even with Modules.
Did I miss something? Is there a thing that you think it’s not clear? Feel free to reach me at the comment section or by message and discuss it!
References
JavaScript Best Practices — Classes and Modules
JavaScript is a very forgiving language. It’s easy to write code that runs but has mistakes in it.
In this article, we’ll look at what should be in our class methods, and also the best ways to use modules.
Class Methods Should Either Reference this Or Be Made Into a Static Method
A JavaScript class can have static or instance methods. If it’s an instance method , then it should reference this . Otherwise, it should be a static method.
For instance, we should write a class that’s like the following code:
class Person < constructor(name) < this.name = name; >static greet() < return 'hi' >greetWithName() < return `hi $`; > >
In the code above, we have the static greet method which doesn’t reference this and the greetWithName method, which references this.name .
Static methods are shared with all instances of the class and instance methods are part of the instance, so it makes sense it references this .
Use ES6 Modules Import and Export Over Non-Standard Module Systems
With ES6, modules are a standard feature of JavaScript. We can divide our code into modules and only export code that we want to expose to the outside.
Also, we can selectively import members from another module.
Before ES6, there’re various module systems like RequireJS and CommonJS. They’re similar to what ES6 modules do today. However, ES6 modules can do what they do and they’re a JavaScript standard.
Therefore, we should just use ES6 modules over other kinds of modules so that we won’t have to worry about those kinds of modules going away or compatibility issues with other kinds of module systems.
For instance, instead of writing the following code to export and import modules:
const < foo >= require("./module"); console.log(foo);
import < foo >from "./module"; console.log(foo);
In the first example, we used module.exports to export a member as a property of an object.
Then we imported the foo property with the require function. This is the old way with CommonJS module which is used before JavaScript has modules as a standard feature.
The second example does the same thing using standard JavaScript modules. We export the member foo in module.js and then import foo in index.js .
JavaScript modules have been a standard for a long time and it’s supported in both browsers and Node.js natively since version 12, we can use regular JavaScript modules anywhere.
If not, we can use transpilers like Browserify, Webpack, or Parcel to transform module code into ES5 code.
Do Not Use Wildcard Imports
Wildcard imports import everything. It’s denoted by an asterisk symbol.
For instance, we do a wildcard import by writing the following code:
import * as module from "./module"; console.log(module.foo);
In the code above, we imported the whole module.js module by using the * and the as keyword to name the module that we imported so that we can reference it in the line below.
This isn’t good because we usually don’t need all the members of a module, so it’s not efficient to import everything.
Instead, we should import only the members that we need. We should write:
import < foo >from "./module"; console.log(foo);
to import just the foo member which we need.
Do Not Export Directly From an Import
We shouldn’t export directly from an import. This means that we shouldn’t use the as keyword to rename the export directly within the export to something else.
It’s clearer to separate them from their own line. For instance, we can write the following code to do that:
import < bar >from "./bar"; console.log(bar);
In the code above, we exported the foo member from module.js as bar by using the export statement.
We shouldn’t do that since it’s not clear that we’re both importing from module.js and exporting the member as bar at the same time.
Instead, we should write the following in bar.js :
import < foo >from "./module"; export < foo as bar >from "./module";
This way, it’s clear that we’re both importing and exporting in one module.
Conclusion
Class methods should reference this if it’s an instance method or be static if they don’t.
We should use ES6 modules since it’s a JavaScript standard. Also, we should separate imports and exports and shouldn’t import everything from a module.