This document is an incomplete draft of a proposed style guide for JavaScript written at Distributive.
Last Updated: July 2021, July 2023
Table of Contents
- You are an intelligent sentient being working on code that you can see. While rules, linters, and style guides are great innovations, they do not have the same common sense you possess. You are free to ignore the style guide, on occasion, when it makes sense to do so.
- We do not change lines unless we are changing code. Incorrectly-indented code, for example, should be left incorrectly indented if it has already been been merged to develop that way, until it needs to be modified. It is more important to preserve the git history than it is to have beautiful code. Proper source code attribution and regular (uncollapzsed) check-ins is one of the techniques I use to keep the tax people from making us fill out time sheets every day! Corollary: please do your best to submit only beautiful code for MR, as it won't be fixed for purely cosmetic reasons later!
- When working in a third-party repository, follow their style guide or prevailing style, as appropriate
- When working in older areas of DCP and making minor changes, you are free to use the existing prevailing style OR the new style, or a blend of both.
- When working in older areas of DCP and making substantial changes, use this style guide for lines of code you are writing or changing.
CommonJS is preferred, either 1.0 or 2.0 with the BravoJS cjs-node shim.
Either ESMs or BravoJS (CommonJS modules 2.0) are acceptable. WebPack is also acceptable, however should only be used if there is a significant technical or maintenance advantage for the specific project; there are WebPack interoperability gotchas with dcp-client etc.
Either traditional ES5 or ES6 class syntax is acceptable.
NodeJS implements a module system which is nearly CommonJS Modules/1.0.0 compliant, however they all several extensions which are not.
The most important property to be aware of is that CommonsJS modules are intended to be singletons, returning only one exports object from the first use of require
to address a given module. Failure to follow this guidance produces modules which can have problems with cyclical dependency graphs.
Do not use: exports object replacement via return
return exports; /* replace exports object */
Do not use: exports object replacement via assignment
module.exports = { some_new_object }; /* replace exports object */
-
Don't decorate the
exports
object viaObject.assign()
with nested declaration. Instead, decorate it viaObject.assign()
without nesting, or via property assignment.// Bad Object.assign(exports, { a: function a() {}, b: function b() {}, }); // Good function a() {} function b() {} Object.assign(exports, { a, b }); // Good exports.a = function a() {} exports.b = function b() {};
- 2 spaces
- no tabs (code point 9) allowed in files
- no carriage-returns (code point 13) allowed in files
- there are no restrictions with respect to using whitespace within statements to improve alignment between programmatic and visual symmetry
- single-line object literals have a space after
{
and before}
- parentheses are neither followed by nor preceded by inner spaces
-
if
,for
, andfunction
keywords are always followed by a space
if ((a + b) > (c + d))
{
print('Hello, world');
}
- File names are
kebab-case
. - Variable names and object property names are
camelCase
- Acronyms are treated like words; if we had named
window.XMLHttpRequest
at Distributive, it would have been calledwindow.xmlHttpRequest
. - Class names are
CamelCaseWithUppercaseFirstCharacter
- Function arguments that are unused should start with _
- "private" API or object properties should start with __
- When a variable is used to store the value of
this
, it should be calledthat
and assigned as close to the top of its scope as possible. - Caught exceptions should be named
error
- Only use the Unicode character set
- Use UTF-16 per JS norms within strings
- A JS string is a series of 16-bit code units
- Use
codePointAt
and friends whenever possible, rather than manipulating surrogate pairs
- Use the UTF-8 transfer encoding for source files, data on disk, network data, etc
- Use of characters above code point 127 (end of US-ASCII) in source code is discouraged, but permitted when they transmit domain-specific information (for example, using the Greek alpha symbol to represent a threshold value in a statistics module).
Either British, Canadian, or American English are acceptable throughout the code base, however public APIs must always use American English.
Referrer is always spelling wrong - referer
. Don't blame me, blame Phillip Hallam-Baker.
Upper case and lower case are each two words.
There are two bracing styles in use simultaneously throughout the Distributive JS Style:
- Lexical braces: always alone on a line. "Allman Style"
- Literal braces: never alone on a line; closing braces always followed by
,
,)
, or;
. "1tbs Style".
Lexical Braces.
function helloWorld()
{
console.log('hello, world');
}
Lexical Braces. Zero-statement blocks may use both braces on one line. One-statement blocks may omit the braces unless the block is below an else statement.
// Good
try
{
for (let i=0; i < 10; i++)
{
console.log(i);
}
}
catch(error) {}
// Good
if (Math.random() > 1)
console.error('something very bad just happened');
// Good
if (a)
console.log('yes');
else
{
console.log('no');
throw new Error('uh-oh');
}
Not Allowed:
// Bad
if (!a)
{
console.log('no');
throw new Error('uh-oh');
}
else
console.log('yes');
Literal Braces. Very short objects may appear on a single line. Trailing commas in property lists are allowed but not required.
var ballDuck = {
type: 'base',
color: 'white',
stiching: { color: 'red' },
};
Same as object literals, but use square brackets in place of literal braces.
var a = [ 1, 2, 3 ];
var b = [ 4,
5,
6,
];
Literal Braces. When combined with an assignment to a property of the module exports
object, either Literal Braces OR Lexical Braces are permitted.
var foo = function foo() {
var a = [1, 2, 3];
};
var bar = () => {
var b = [4, 5, 6];
};
module.exports.foo2 = function foo2() {
var a = [1, 2, 3];
};
Comments should explain what the program, module, or block does. They should not explain JavaScript syntax or APIs of other units.
- // comments are used
- for linter hints, or
- to temporarily eliminate small blocks of code during debugging.
- multiline comments are written
/* information
* more information
*/
- do not comment a closing brace unless
a) the opening brace it is paired with is at the incorrect indentation level; for example, when the module.declare boilerplate is used but the module code still begins at column 0, or b) the opening brace is more than 50 line of code away (Note that this generally means that your program should be refactored for a flatter layout)
- All statements end in semicolons, except function declarations
- Do not shadow function arguments
- Avoid destructured assignment unless it adds clarity or otherwise simplifies code
- Avoid nested destructured assignment like the plague
- string literals should be declared with single quotes, unless they contain single quotes, in which case, they should be declared with double quotes
console.log('hello, world');
console.log("nice day, isn't it?");
- Use of undeclared variables is not permitted unless accompanied by eslint markup and a comment
- comment not required for variables appearing in our
.eslintrc.js
, such asdcpConfig
andwindow
.
- comment not required for variables appearing in our
- All variables, except those declared
const
, must be manually hoisted to the top of their scope -
var
orconst
declarations are preferred for variables which have module-wide or function scope - "Do not touch" variables -- eg pseudo-private properties -- should begin with double underscore,
__
.
- Whenever possible, functions should have names. This often means that full function syntax is preferred over lightweight (fat arrow) functions when using function literals, provided that doing so does not increase code complexity.
- Avoid closing over
const that=this
; use lightweight functions instead if that is what you need. - Unused function arguments begin with
_
-
loop invariants should be declared with
const
orlet
(nevervar
) within the loop initializer whenever possible.for (let i = 0; i < 10; i++) { // ... }
Lexical bracing may be used within case statements, provided it is used consistently throughout a given switch statement.
- The tested expression in a switch statement must be variable.
switch (true)
is expressly prohibited. - All fall-through cases must be commented with the word "fallthrough" so that code-readers know the fallthrough was purposeful, unless there are zero statements between cases.
- Switch statements with no default case are permitted.
- Switch labels push the indent level.
- A break statement is required in the last case statement.
switch (type)
{
case 'medicine':
usedForFun = false; // fall through
case 'base':
case 'volley':
colour = 'white'; // fall through
case 'basket':
round = true;
break;
case 'foot':
round = false;
break;
}
Long expressions not joined by &&
and ||
should break so the operator starts on the second line and starting in the same column as the beginning of the expression in the first line. This applies to ?:
, binary arithmetic operators including +
, and member-of operators. Rationale: an operator at the front of the continuation line makes for faster visual scanning, as there is no need to read to the end of line. Also there exists a context-sensitive keyword hazard in JavaScript (https://bugzilla.mozilla.org/show_bug.cgi?id=442099) which can be avoided by putting .
at the start of a continuation line, in long member expression.
==
is preferred to ===
Unary keyword operators, such as typeof
, should not have their operand parenthesized; e.g. use typeof 'foo' === 'string'
.
Types should be coerced as explicitly as possible
- avoid use of
==
and friends, use===
etc - if
==
must be used for a coerced comparison, a comment is probably warranted. - avoid shortcut operators like
!!abc
and+xyz
; useBoolean(abc)
andNumber(xyz)
instead.
-
Prefer
then(onFulfilled).catch(onRejected)
overthen(onFulfilled, onRejected)
.Why? In the second form,
onFulfilled()
rejecting isn't caught beonRejected()
. Although, you know, if you specifically need the first behaviour, go ahead and use it, but if the reason is not obvious it probably deserves a comment.
Always write comparisons with laser precision:
- If you want to know if x is bigger than a, use
if (x > a)
. - If you want to know if x is not bigger than a, use
if (!(x > a))
NOTif (x <= a)
.... unless your test also tests to make sure that both x and are numbers which are not NaN. - Do not compare
x == true
orx == false
. Use(x)
or(!x)
instead.x == true
, is certainly different from if(x)
! Compare objects tonull
, numbers to0
or strings to""
, if there is chance for confusion.
Is never a good idea. Inherit from the standard classes instead.
Avoid multi-tier function chaining when possible. Use intermediate variables to break up the chain. This makes it easier to inspect links in the chain with the debugger, and provides an opportunity for documentation via naming.
- Beautiful is better than ugly.
- Explicit is better than implicit.
- Simple is better than complex.
- Complex is better than complicated.
- Flat is better than nested.
- Sparse is better than dense.
- Readability counts.
- Special cases aren't special enough to break the rules.
- Although practicality beats purity.
- Errors should never pass silently.
- Unless explicitly silenced.
- In the face of ambiguity, refuse the temptation to guess.
- There should be one -- and preferably only one -- obvious way to do it.
- Now is better than never.
- Although never is often better than right now.
- If the implementation is hard to explain, it's a bad idea.
- If the implementation is easy to explain, it may be a good idea.
There is no reason in DCP to use any JavaScript dialect which predates ES-5, including detecting excessively-old engines. You are free to pretend these do not exist unless unless you have a very, very, very specific use-case.
This code should be avoided, unless it is technically infeasible to so. Note that source files cannot be naively converted from non-strict to strict; they must be thoroughly tested during the maintenance cycle.
Never count on automatic hoisting of functions declared in block-level statements in ES-5 non-strict code: you can create cross-browser bugs here, as the behaviour was underspecified in ES-3 and remains so in ES-5 non-strict. See https://bugzilla.mozilla.org/show_bug.cgi?id=585536 for a full treatise.
Do Not Use: this code throws a reference error in some implementations because returnData is undefined.
exports.getGroups = function (x)
{
{
var query = { success: returnData, error: cbFailure, method: 'get', table: 'Token' };
query.prototype = { value: x[index] };
for (let index = 0; index < x.length; index++)
{
function cbFailure(res) { }
function returnData(data) { }
db.act(query);
}
}
}
- Begin with comment block
- @file - description
- @author - principal author(s)
- @date - date corresponding with start of primary work
- Do not have
.js
extension - Have execute permissions
- begin with
#! /usr/bin/env node
Example Program:
#! /usr/bin/env node
/**
* @file cgadm Compute Group Administration tool
* A tool for administering compute groups for scheduler sysadmins
* @author Wes Garland, wes@distributive.network
* @date June 2021
*/
'use strict';
const path = require('path');
const process = require('process');
async function main()
{
var r = Math.random();
console.log('Hello, world');
for (let i=0; i < r*100; i++)
{
if (i % 3 === 0)
console.log('hello', i);
}
}
- When working with an
EventEmitter
-like object, useaddEventListener
andremoveEventListener
.Why?
EventEmitter
-like objects have 3 different ways of doing the same thing:addEventListeer
/removeEventListener
,addListener
/removeListener
, andon
/off
. Sticking with the naming convention of the interface for theEventTarget
Web API results in a more consistent codebase.