16 KiB
title |
---|
JavaScript Quality Guide |
Goal
This style guide aims to provide the ground rules for an application's JavaScript code, such that it's highly readable and consistent across different developers on a team. The focus is put on quality and coherence across the different pieces of your application.
Table of Contents
- Modules
- Strict Mode
- Spacing
- Semicolons
- Linting
- Strings
- Variable Declaration
- Conditionals
- Equality
- Ternary Operators
- Functions
- Prototypes
- Object Literals
- Array Literals
- Regular Expressions
- Comments
- Variable Naming
- Everyday Tricks
Modules
The most common module systems are CommonJS, AMD, and ES6 Modules. Modules systems provide individual scoping, avoid leaks to the global
object, and improve code base organization by automating dependency graph generation, instead of having to resort to manually creating multiple <script>
tags.
Module systems also provide us with dependency injection patterns, which are crucial when it comes to testing individual components in isolation.
Strict Mode
Always put "use strict";
at the top of your modules. Strict mode allows you to catch nonsensical behavior, discourages poor practices, and is faster because it allows compilers to make certain assumptions about your code.
Spacing
Spacing must be consistent across every file in the application. To this end, using something like .editorconfig
configuration files is highly encouraged. Here are the defaults used for lbry.tech:
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
The .editorconfig
file takes care of creating the desired spacing for this project by pressing the tab key.
Spacing doesn't just entail tabbing, but also the spaces before, after, and in between arguments of a function declaration. Here are some examples of the spacing scheme we employ:
function () {}
function (a, b) {}
function namedFunction(a, b) {}
Semicolons;
We love using semicolons to avoid potential issues with Automatic Semicolon Insertion (ASI) and to save us from ourselves.
Linting
Linting is great for ensuring basic mistakes don't get merged into master (like unnecessary or unintentional semicolons). We use eslint
to ensure our next-generation JavaScript code is up to snuff.
Here is our .eslintrc.json
file:
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"rules": {
"indent": ["error", 2, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"]
}
}
Strings
Strings should always be quoted using the same quotation mark throughtout a codebase. Be consistent! We prefer double-quotes.
Bad
const message = 'oh hai ' + name + "!";
Good
const message = "oh hai " + name + "!";
You'll be a happier JavaScript developer if you use template literals. That way it'll be far easier to format your strings, and the code looks a lot cleaner too.
Better
const message = util.format(`oh hai ${name}!`);
To declare multi-line strings, particularly when dealing with HTML snippets, template literals definitely come in handy!
const html = `
<div>
<span class='monster'>${name}</span>
</div>
`;
Variable Declaration
Always declare variables in a consistent manner, and at the top of their scope. Keeping variable declarations to one per line is encouraged. Comma-first, a single var
statement, multiple var
statements, it's all fine, just be consistent across the project, and ensure the team is on the same page.
Knowing when to use const
, let
and var
is super important.
Bad
const foo = 1,
bar = 2;
let baz;
let pony;
let a
, b;
const foo = 1;
if (foo > 1) {
var bar = 2;
}
Good
const foo = 1;
const bar = 2;
let baz;
let pony;
let a;
let b;
const foo = 1;
let bar;
if (foo > 1) {
bar = 2;
}
Variable declarations that aren't immediately assigned a value are acceptable to share the same line of code.
Acceptable
const a = "a";
const b = 2;
let i, j;
Conditionals
If you are one-lining conditionals, the conditional should be a) short and b) without brackets. Otherwise, brackets are enforced for the sake of text comprehension.
Acceptable
if (err) throw err;
Better
if (err) {
throw err;
}
Equality
Avoid using ==
and !=
operators, always favor ===
and !==
. These operators are called the "strict equality operators," while their counterparts will attempt to cast the operands into the same value type.
Bad
function isEmptyString (text) {
return text == "";
}
isEmptyString(0);
// <- true
Good
function isEmptyString(text) {
return text === "";
}
isEmptyString(0);
// <- false
Ternary Operators
Ternary operators are fine for clear-cut conditionals, but unacceptable for confusing choices. As a rule, if you can't eye-parse it as fast as your brain can interpret the text that declares the ternary operator, chances are it's probably too complicated for its own good.
Use operators responsibly.
jQuery is a prime example of a codebase that's filled with nasty ternary operators.
Bad
function calculate(a, b) {
return a && b ? 11 : a ? 10 : b ? 1 : 0;
}
Good
function getName (mobile) {
return mobile ? mobile.name : "Generic Player";
}
In cases that may prove confusing just use if
and else
statements instead. If you have more than three if
/else
statements, switch cases are the way to go for readability/comprehension.
Functions
When declaring a function, always use the function declaration form instead of function expressions. Because hoisting.
Bad
const sum = (x, y) => {
return x + y;
};
Good
function sum(x, y) {
return x + y;
}
That being said, there's nothing wrong with function expressions that are just currying another function.
Good
const plusThree = sum.bind(null, 3);
Keep in mind that function declarations will be hoisted to the top of the scope so it doesn't matter the order they are declared in. That being said, you should make sure to avoid placing them inside conditional statements.
Bad
if (Math.random() > 0.5) {
sum(1, 3);
function sum(x, y) {
return x + y;
}
}
Good
if (Math.random() > 0.5) {
sum(1, 3);
}
function sum(x, y) {
return x + y;
}
function sum(x, y) {
return x + y;
}
if (Math.random() > 0.5) {
sum(1, 3);
}
If you need a "no-op" method you can use either Function.prototype
, or function noop () {}
. Ideally a single reference to noop
is used throughout the application.
Don't declare functions inside of loops.
Bad
const values = [1, 2, 3];
for (let i = 0; i < values.length; i++) {
setTimeout(() => {
console.log(values[i]);
}, 1000 * i);
}
const values = [1, 2, 3];
for (let i = 0; i < values.length; i++) {
setTimeout(i => {
return function () {
console.log(values[i]);
};
}(i), 1000 * i);
}
Good
const values = [1, 2, 3];
for (let i = 0; i < values.length; i++) {
setTimeout(i => {
console.log(values[i]);
}, 1000 * i, i);
}
const values = [1, 2, 3];
for (let i = 0; i < values.length; i++) {
wait(i);
}
function wait(i) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
}
Better
const values = [1, 2, 3];
for (const value of values) {
setTimeout(() => {
console.log(value);
}, 1000 * value);
}
Whenever a method is non-trivial, make the effort to use a named function declaration rather than an anonymous function. This will make it easier to pinpoint the root cause of an exception when analyzing stack traces.
Bad
function once(fn) {
let ran = false;
return function () {
if (ran) return;
ran = true;
fn.apply(this, arguments);
};
}
Good
function once(fn) {
let ran = false;
return function run() {
if (ran) return;
ran = true;
fn.apply(this, arguments);
};
}
Avoid keeping indentation levels from raising more than necessary by using guard clauses instead of flowing if
statements.
Bad
if (car) {
if (black) {
if (turbine) {
return "Batman!";
}
}
}
if (condition) {
// 10+ lines of code
}
Good
if (!car) {
return;
}
if (!black) {
return;
}
if (!turbine) {
return;
}
return "Batman!";
if (!condition) {
return;
}
// 10+ lines of code
Prototypes
Hacking native prototypes should be avoided at all costs, use a method instead.
Bad
String.prototype.half = function () {
return this.substr(0, this.length / 2);
};
Good
function half(text) {
return text.substr(0, text.length / 2);
}
Avoid prototypical inheritance models unless you have a very good performance reason to justify yourself.
- Prototypical inheritance boosts puts need for
this
through the roof - It's way more verbose than using plain objects
- It causes headaches when creating
new
objects - Needs a closure to hide valuable private state of instances
- Just use plain objects instead
Object Literals
Instantiate using curly braced notation {}
and use factories instead of constructors. Here's a proposed pattern for you to implement objects in general.
function util(options) {
// private methods and state go here
let foo;
function add() {
return foo++;
}
function reset() { // note that this method isn't publicly exposed
foo = options.start || 0;
}
reset();
return {
// public interface methods go here
uuid: add
};
}
Array Literals
Instantiate using the square bracketed notation []
. If you have to declare a fixed-dimension array for performance reasons then it's fine to use the new Array(length)
notation instead.
It's about time you master array manipulation! Learn about the basics. It's way easier than you might think.
Learn and abuse the functional collection manipulation methods. These are so worth the trouble.
Regular Expressions
Keep regular expressions in variables, don't use them inline. This will vastly improve readability.
Bad
if (/\d+/.test(text)) {
console.log("so many numbers!");
}
Good
const numeric = /\d+/;
if (numeric.test(text)) {
console.log("so many numbers!");
}
Also learn how to write regular expressions, and what they actually do. Then you can also visualize them online.
Comments
For code that isn't super obvious, we write comments to explain what it does. The exception to that rule is explaining what a regular expression does.
Bad
// create the centered container
const p = $("<p/>");
p.center(div);
p.text("foo");
Good
const container = $("<p/>");
const contents = "foo";
container.center(parent);
container.text(contents);
megaphone.on("data", value => {
container.text(value); // the megaphone periodically emits updates for container
});
const numeric = /\d+/; // one or more digits somewhere in the string
if (numeric.test(text)) {
console.log("so many numbers!");
}
Variable Naming
Variables have meaningful names so that we don't have to resort to commenting what a piece of functionality does. We try to be expressive while succinct, and use meaningful variable names.
Bad
function a(x, y, z) {
return z * y / x;
}
a(4, 2, 6);
// <- 3
Good
function ruleOfThree(had, got, have) {
return have * got / had;
}
ruleOfThree(4, 2, 6);
// <- 3
Everyday Tricks
We use ||
to define default values. If the left-hand value is falsy then the right-hand value will be used.
function a(value) {
const defaultValue = 33;
const used = value || defaultValue;
}