Key Takeaways
- Use structural techniques such as moving code into a function, replacing conditional expressions with functions, and using pure functions to make JavaScript code self-documenting and easier to understand.
- Follow naming conventions and use meaningful names for variables, functions, and classes to make the code more readable and self-explanatory.
- Use syntax-related methods such as avoiding syntax tricks, using named constants, and leveraging language features to make the code clearer.
- While self-documenting code can reduce the need for comments and improve the maintainability of the code, it does not replace the need for good comments and comprehensive documentation.
- Be cautious about extracting code for the sake of having short functions and avoid forcing things if they don’t seem to be a good idea, as these practices can hinder the understandability of the code.
Overview of Techniques
Some programmers include comments as part of self-documenting code. In this article, we’ll only focus on code. Comments are important, but they’re a large topic to be covered separately. We can split the techniques for self-documenting code into three broad categories:- structural, where the structure of code or directories is used to clarify the purpose
- naming related, such as function or variable naming
- syntax related, where we make use of (or avoid using) features of the language to clarify code.
Structural
First, let’s look at the structural category. Structural changes refer to shifting code around for enhanced clarity.Move code into a function
This is the same as the “extract function” refactoring — meaning that we take existing code and move it into a new function: we “extract” the code out into a new function. For example, try to guess what the following line does:var width = (value - 0.5) * 16;
Not very clear; a comment here could be quite useful. Or, we could extract a function to make it self documenting:
var width = emToPixels(value);
function emToPixels(ems) {
return (ems - 0.5) * 16;
}
The only change was I moved the calculation into a function. The function’s name is descriptive of what it does, so the code no longer needs clarification. As an additional benefit, we now have a useful helper function that you can use elsewhere, so this method also helps reduce duplication.
Replace conditional expression with function
If clauses with multiple operands can often be hard to understand without a comment. We can apply a similar method as above to clarify them:if(!el.offsetWidth || !el.offsetHeight) {
}
What is the purpose of the above condition?
function isVisible(el) {
return el.offsetWidth && el.offsetHeight;
}
if(!isVisible(el)) {
}
Again, we moved the code into a function and the code is immediately much easier to understand.
Replace expression with variable
Replacing something with a variable is similar to moving code into a function, but instead of a function, we simply use a variable. Let’s take a look at the example with if clauses again:if(!el.offsetWidth || !el.offsetHeight) {
}
Instead of extracting a function, we can also clarify this by introducing a variable:
var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}
This can be a better choice than extracting a function — for example, when the logic you want to clarify is very specific to a certain algorithm used only in one place.
The most common use for this method is mathematical expressions:
return a * b + (c / d);
We can clarify the above by splitting the calculation:
var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;
Because I’m terrible at math, imagine the above example has some meaningful algorithm. In any case, the point is that you can move complex expressions into variables that add meaning to otherwise hard-to-understand code.
Class and module interfaces
The interface — that is, the public methods and properties — of a class or module can act as documentation on its usage. Let’s look at an example:class Box {
setState(state) {
this.state = state;
}
getState() {
return this.state;
}
}
This class could contain some other code in it as well. I purposely kept the example simple, to illustrate how the public interface is documentation
Can you tell how this class should be used? Maybe with a little bit of work, but it isn’t very obvious.
Both of the functions have reasonable names: what they do is clear from their name. But despite this, it’s not very clear how you should be using them. Most likely you would need to read more code or the documentation for the class to figure it out.
What if we changed it to something like this:
class Box {
open() {
this.state = 'open';
}
close() {
this.state = 'closed';
}
isOpen() {
return this.state === 'open';
}
}
Much easier to see the usage, don’t you think? Notice that we only changed the public interface; the internal representation is still the same with the this.state
property.
Now you can tell at a glance how the Box
class is used. This shows that even though the first version had good names in the functions, the complete package was still confusing, and how, with simple decisions like this, you can have a very big impact. You always have to think of the big picture.
Code grouping
Grouping different parts of code can also act as a form of documentation. For example, you should always aim to declare your variables as close to where they are being used as possible, and try to group variable uses together. This can be used to indicate a relationship between the different parts of the code, so that anyone changing it in the future has an easier time finding which parts they may also need to touch. Consider the following example:var foo = 1;
blah()
xyz();
bar(foo);
baz(1337);
quux(foo);
Can you see at a glance how many times foo
was used? Compare it to this:
var foo = 1;
bar(foo);
quux(foo);
blah()
xyz();
baz(1337);
With all the uses of foo
grouped together, we can easily see which parts of the code depend on it.
Use pure functions
Pure functions are much easier to understand than functions that rely on state. What is a pure function? When calling a function with the same parameters, if it always produces the same output, it’s most likely a so-called “pure” function. This means the function should not have any side effects or rely on state — such as time, object properties, Ajax, etc. These types of functions are easier to understand, as any values affecting their output are passed in explicitly. You won’t have to dig around to figure out where something comes from, or what affects the result, as it’s all in plain sight. Another reason these types of functions make for more self-documenting code is you can trust their output. No matter what, the function will always return output only based on what parameters you give it. It also won’t affect anything external, so you can trust it won’t cause an unexpected side effect. A good example of where this goes wrong isdocument.write()
. Experienced JS developers know you shouldn’t use it, but many beginners stumble with it. Sometimes it works well — but other times, in certain circumstances, it can wipe the whole page clean. Talk about a side effect!
For a better overview of what a pure function is, see the article Functional Programming: Pure Functions.
Directory and file structure
When naming files or directories, follow the same naming convention as used in the project. If there’s no clear convention in the project, follow the standard for your language of choice. For example, if you’re adding new UI-related code, find where similar functionality is in the project. If UI-related code is placed insrc/ui/
, you should do the same.
This makes it easier to find the code and shows its purpose, based on what you already know about the other pieces of code in the project. All UI code is in the same place, after all, so it must be UI related.
Naming
There’s a popular quote about the two hard things in computer science:There are only two hard things in Computer Science: cache invalidation and naming things. — Phil KarltonSo let’s take a look at how we can use naming things to make our code self documenting.
Rename function
Function naming is often not too difficult, but there’s some simple rules that you can follow:- Avoid using vague words like “handle” or “manage”:
handleLinks()
,manageObjects()
. What do either of these do? - Use active verbs:
cutGrass()
,sendFile()
— functions that actively perform something. - Indicate return value:
getMagicBullet()
,readFile()
. This is not something you can always do, but it’s helpful where it makes sense. - Languages with strong typing can use type signatures to help indicate return values as well.
Rename variable
With variables, here are two good rules of thumb:- Indicate units: if you have numeric parameters, you can include the expected unit. For example,
widthPx
instead ofwidth
to indicate the value is in pixels instead of some other unit. - Don’t use shortcuts:
a
orb
are not acceptable names, except for counters in loops.
Follow established naming conventions
Try to follow the same naming conventions in your code. For example, if you have an object of a specific type, call it the same name:var element = getElement();
Don’t suddenly decide to call it a node:
var node = getElement();
If you follow the same conventions as elsewhere in the codebase, anyone reading it can make safe assumptions about the meanings of things based on what it means elsewhere.
Use meaningful errors
Undefined is not an object! Everyone’s favorite. Let’s not follow JavaScript’s example, and let’s make sure any errors our code throws have a meaningful message in them. What makes an error message meaningful?- it should describe what the problem was
- if possible, it should include any variable values or other data which caused the error
- key point: the error should help us find out what went wrong — therefore acting as documentation on how the function should work.
Syntax
Syntax-related methods for self-documenting code can be a little bit more language specific. For example, Ruby and Perl allow you to do all kinds of strange syntax tricks, which, in general, should be avoided. Let’s take a look at a few that happen with JavaScript.Don’t use syntax tricks
Don’t use strange tricks. Here’s a good way to confuse people:imTricky && doMagic();
It’s equivalent to this much more sane looking code:
if(imTricky) {
doMagic();
}
Always prefer the latter form. Syntax tricks are not going to do anyone any favors.
Use named constants, avoid magic values
If you have special values in your code — such as numbers or string values — consider using a constant instead. Even if it seems clear now, more often than not, when coming back to it in a month or two, nobody will have any idea why that particular number was put there.const MEANING_OF_LIFE = 42;
(If you’re not using ES6, you can use a var
and it’ll work equally well.)
Avoid boolean flags
Boolean flags can make for hard-to-understand code. Consider this:myThing.setData({ x: 1 }, true);
What is the meaning of true
? You have absolutely no idea, unless you dig into the source for setData()
and find out.
Instead, you can add another function, or rename an existing function:
myThing.mergeData({ x: 1 });
Now you can immediately tell what’s going on.
Use language features to your advantage
We can even use some features of our chosen language to better communicate the intention behind some code. A good example of this in JavaScript are the array iteration methods:var ids = [];
for(var i = 0; i < things.length; i++) {
ids.push(things[i].id);
}
The above code collects a list of IDs into a new array. However, in order to know that, we need to read the whole body of the loop. Compare it with using map()
:
var ids = things.map(function(thing) {
return thing.id;
});
In this case, we immediately know that this produces a new array of something, because that’s the purpose of map()
. This can be beneficial especially if you have more complicated looping logic. There’s a list of other iteration functions on MDN.
Another example with JavaScript is the const
keyword.
Often, you declare variables where the value is supposed to never change. A very common example is when loading modules with CommonJS:
var async = require('async');
We can make the intention of never changing this even more clear:
const async = require('async');
As an added benefit, if someone ever accidentally tries to change this, we’ll now get an error.
Anti-patterns
With all these methods at your disposal, you can do a lot of good. However, there are some things you should be careful about …Extracting for the sake of having short functions
Some people advocate the use of tiny tiny functions, and if you extract everything out, that’s what you can get. However, this can detrimentally affect how easy the code is to understand. For example, imagine you’re debugging some code. You look in functiona()
. Then, you find it uses b()
, which then uses c()
. And so on.
While short functions can be great and easy to understand, if you’re only using the function in a single place, consider using the “replace expression with variable” method instead.
Don’t force things
As usual, there’s no absolute right way to do this. Therefore, if something doesn’t seem like it’s a good idea, don’t try to force it.Conclusion
Making your code self documenting goes a long way to improving the maintainability of your code. Every comment is additional cruft that has to be maintained, so eliminating comments where possible is a good thing. However, self-documenting code doesn’t replace documentation or comments. For example, code is limited in expressing intent, so you need to have good comments as well. API documentation is also very important for libraries, as having to read the code is not feasible unless your library is very small.Frequently Asked Questions (FAQs) about Self-Documenting JavaScript
What is the importance of self-documenting code in JavaScript?
Self-documenting code is crucial in JavaScript as it enhances readability and maintainability. It allows developers to understand the code’s purpose without needing extensive external documentation. This is particularly beneficial in large projects or when the code is being handled by multiple developers. It reduces the time spent on understanding the code, thus increasing productivity. Moreover, it reduces the risk of errors during code modification as the code’s purpose and functionality are clear.
How can I write self-documenting code in JavaScript?
Writing self-documenting code in JavaScript involves several practices. Firstly, use meaningful names for variables, functions, and classes. The names should clearly indicate their purpose. Secondly, keep the code structure simple and consistent. Avoid complex and nested structures. Thirdly, use comments sparingly and only when necessary to explain complex logic. Lastly, follow established coding conventions and standards for JavaScript to ensure consistency and readability.
Can self-documenting code replace well-documented code?
While self-documenting code improves readability and understanding, it cannot entirely replace well-documented code. Documentation provides a broader context, including the code’s purpose, its interaction with other parts of the system, and any known issues or limitations. Self-documenting code complements documentation by making the code easier to understand at a granular level.
What are the limitations of self-documenting code?
While self-documenting code has many benefits, it also has limitations. It may not adequately convey the purpose or functionality of complex algorithms or logic. It also relies on the developer’s ability to choose meaningful names and maintain a simple and consistent structure. Moreover, it does not provide the broader context that documentation provides.
How does self-documenting code contribute to code quality?
Self-documenting code contributes to code quality by enhancing readability, maintainability, and reducing errors. It makes the code easier to understand, modify, and debug. It also promotes consistency in coding practices, which is a key aspect of code quality.
How can I improve my self-documenting code skills?
Improving self-documenting code skills involves practice and learning from others. Regularly write code and focus on making it self-documenting. Review code written by others to learn from their practices. Participate in code reviews to get feedback on your code. Also, learn and follow established coding conventions and standards.
Can self-documenting code help in agile development?
Yes, self-documenting code is particularly beneficial in agile development. It allows for faster understanding and modification of the code, which is crucial in agile’s iterative and incremental development approach. It also reduces the need for extensive documentation, allowing the team to focus more on developing working software.
How does self-documenting code relate to clean code?
Self-documenting code is a key aspect of clean code. Clean code is readable, simple, and concise. It uses meaningful names, has a consistent structure, and avoids unnecessary complexity – all characteristics of self-documenting code.
Can self-documenting code reduce the need for code comments?
Yes, one of the goals of self-documenting code is to reduce the need for code comments. By making the code’s purpose and functionality clear through its structure and naming, the need for comments explaining the code is reduced. However, comments may still be necessary to explain complex logic or provide broader context.
How does self-documenting code benefit code reviews?
Self-documenting code makes code reviews more efficient. Reviewers can understand the code’s purpose and functionality more quickly, allowing them to focus on identifying issues or improvements. It also reduces the risk of misunderstandings or misinterpretations during the review.
Jani has built all kinds of JS apps for more than 15 years. At his blog, he helps JavaScript developers learn to eliminate bad code so they can focus on writing awesome apps and solve real problems.