Modules are the Basic Unit of Code Organization in JavaScript
So my last post started by saying that it may be a good idea to avoid using “class” syntax in JS because it is unnecessary and potentially misleading, being merely syntax sugar for “OLOO” (objects linked to other objects) prototype delegation, which is a fundamentally different mechanism and paradigm from classical OOP class-based inheritance.
So what should you do instead? As I mentioned in that post, there are a variety of options, but above all, most of the time, you will be using modules.
I’m currently reading one of Kyle Simpson’s latest books, and it’s confirming what I’ve been thinking for a while now: That the module is to JS as the class is to Java:
The module pattern has essentially the same goal as the class pattern, which is to group data and behavior together into logical units. Also like classes, modules can “include” or “access” the data and behaviors of other modules, for cooperation’s sake. (p. 59)
In Java, the rules of the language require you to define one class per file. Anything else is simply not valid Java. In JS, we don’t have such silly arbitrary restrictions. However, due to the way JS and the module system works, whenever you import stuff from one file to use in another, you get something that is functionally very much like a “class”.
Let’s back up, and refactor a basic example class with the classic revealing module pattern.
Now, there are few things going on in the second example which may be confusing if you’re not yet used to this sort of thing. I used fat arrow syntax to define the function. You could use the function keyword. There are some differences, but none that matter here, so it’s a stylistic choice. There also several choices I could have made with the inc function, none of which matter here.
Also, the underscore (_) in _count is unnecessary here, and ordinarily I wouldn’t use it; I just want to make it extra clear that you can’t actually access that variable outside the function, even though I’m using a getter to make it look like you can.
The function is immediately invoked; it is an IIFE (immediately-invoked function expression). It just skips the step of defining the function and then calling it; it is immediately called, and its return value is assigned to the const moduleCounter. This is useful if it is meant to be used like a “singleton” (we only want one of it).
If we want to have to multiple instances, we just won’t wrap it an an IIFE: it will be a factory function that we can invoke any time we want (without the new keyword) to get a new instance any time want:
I also added the ability to supply an initial value for the counter, while still defaulting to 0 if one is not provided. A common practice is to supply a single object as a parameter instead of a parameter list. That way each parameter gets a name in the form of an object key that you have to use when supplying the parameter. Even though it requires a bit more typing, it makes the code more self-documenting, and if you want to add or remove parameters in the future, you can easily do so without breaking anything.
Putting the object’s keys in the parameter list makes use of parameter destructuring, so that we can supply the function with an argument like so: makeCounter({ counter: 5 }), but within the body of the function, we can refer to counter directly, exactly as if we had done let counter = whatever on the very first line of the function.
{count=0} ={} looks weird, but it’s just ES6 trickery for supplying defaults. Without the “= {}” part, if we call the function with no arguments, it will throw an error, because we’re trying to destructure “count” from undefined, which isn’t allowed. With “= {}”, undefined will be replaced with an empty object. Trying to destructure a key that an object doesn’t have isn’t a problem; it will just get a value of undefined, but since we’ve provided a default, it will get that value instead.
That’s useful and fun ES6 syntax that is worth learning, but if it’s confusing, just know the first line could be changed to const makeCounter = (counter=0) => { and everything else will work exactly the same, except to use it, we’d have to do makeCounter() or makeCounter(5), instead of makeCounter() or makeCounter({count: 5}).
Also, since I no longer need any statements before the return statement, I can just put the return value directly after the fat arrow (=>), but since it’s an object literal, it needs to be wrapped in parentheses. Otherwise, the curly braces would be interpreted as a code block which would expect a return statement like before, rather than an object literal return value.
See here for a more thorough and perhaps somewhat clearer walkthrough of how all this works.
TL;DR: in the first module example, we created the equivalent of a “private member” by declaring a local variable in the function’s scope. Thanks to the way closures work, the methods on the returned object (inc and the getter) will always have access to it, but there is no other way to access it from “outside” the function. A parameter is effectively the same thing as a variable declaration/assignment, so taking a parameter and just assigning it to a local variable is pointless and redundant, especially since ES6 gives a convenient way to provide a default value.
So this factory module can do anything a class can do, using the native logic of JS instead of syntax sugar that apes the appearance of Java code for no discernable benefit.
But, well, is there a discernable benefit? Prototype delegation (and the class syntax sugar that utilizes it) allows us to share methods by storing them on only one object, and then creating many other objects that can use them as if they were their own. But when we use a factory function to create objects with methods as in the previous example, every object will have its own copy of the methods.
That is an important difference to be aware of, and could make an important difference in some situations. However, premature optimization is the root of much evil. Unless you are creating thousands of instances, which we hardly ever do in most applications, the impact will be negligible.
Ok, so that’s the revealing module/factory function patterns. Now let’s see what happens if we import from one file to another. I’ll return to the very simplest version, because it helps illustrate my point more clearly, which is that:
ESMs are, in effect, “singletons,” in that there’s only one instance ever created, at first import in your program, and all other imports just receive a reference to that same single instance. If your module needs to support multiple instantiations, you have to provide a classic module-style factory function on your ESM definition for that purpose. (p. 63).
And then we’ll import and use it:
I’m using ES6 modules in Node, but it works the same with CommonJS, etc. Note that it behaves identically to our first example. That’s because due to the way the module system works, it’s effectively as if when we do the “import” statement, everything in that file is wrapped in an IIFE and then executed.
We don’t have to worry about that let statement “polluting global scope” or anything like that… It is in the module’s scope. Exactly like when we wrote our own IIFE and declared variables inside it to create the equivalent of private members. It’s already in the module, so wrapping it inside anything in this case would be pointless and redundant.
So this is part of what annoys me about unthinking superfluous use of “class” in JS. If you need instances, and you really want them to share methods, maybe consider using prototype delegation, with or without the “class” keyword… (But also consider using a factory function instead.)
But don’t make everything a class just because that is how some completely different programming languages force you to do it. It’s pointless and makes no sense.
I actually once encountered, in an Angular project, a service class that was actually just a stateless bag of functions. Which is of course contained in a module, as everything is. So basically just a module of exported helper functions, wrapped in layer upon layer of pointless redundancy, Inception-style. Please don’t do that.
Listen to the guy who’s forgotten more about JS than most people will ever know:
As shown, ES modules can use classic modules internally if they need to support multiple-instantiation. Alternatively, we could have exposed a class from our module instead of a create(..) factory function, with generally the same outcome. However, since you’re already using ESM at that point, I’d recommend sticking with classic modules instead of class.
If your module only needs a single instance, you can skip the extra layers of complexity: export its public methods directly. (pp. 65–66)
Quotes from Simpson, Kyle. You Don’t Know JS Yet: Get Started. GetiPub & Leanpub. Kindle Edition.