JavaScript Style Guide
Given the positive experience with TypeScript while writing the VS Code xPack extension, starting in 2021, existing JavaScript projects will be migrated to TypeScript.
Standard TS/JS
After many years of working with style guides for various languages, the conclusion is that the specific style is of lesser importance; what matters more is its consistent application.
To ensure consistency in the xPack JavaScript source files, the primary requirement is to pass the Standard JS validation tools, which also have a TypeScript variant (ts-standard).
Following this, the main recommendations are:
- Utilise the ECMAScript 6 specifications (ES 6).
- If the module performs reasonably complex tasks, its public functions must be asynchronous.
- Asynchronous functions should use promises (and definitely avoid callbacks).
- Reentrancy should be carefully considered (avoid module-global variables).
The xPack project preferences
Prefer TypeScript over JavaScript
This is Rule No. 1, which overrides all other rules.
If JavaScript code must still be used in some instances, prefer ES6 solutions.
Definitely avoid using old-style code.
Use classes as much as possible
Even if the new syntax is mostly syntactic sugar, and internally things behave as strangely as they did in the first JavaScript versions, still use the new class syntax at large; it is much cleaner and improves readability.
Use promises instead of callbacks
Really. No callbacks at all. Use promises. Actually use async
/await
.
Use async/await for asynchronous calls
Once async
/await
became standard, and the V8 engine added support
for them, there is no reason for not using async
/await
.
Wrap old legacy code using callbacks into promises and execute
them with await
.
Use static class members for sharing
Modules are singletons; using module variables is like using static variables in a multi-threaded environment; they may provide a way of sharing common data between multiple instances of objects created inside the same module, but if not handled correctly this may have unexpected results.
The general recommendation is to make the modules re-entrant. In practical terms, do not use module-global variables at all; make the module export a class, and create instances of it whenever needed; for sharing data between instances, use static class members.
Do not restrict export to a single function or class
Bad style:
module.exports = function () {
return /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
};
...
const func = require('name')
Apart from being unnamed, returning a single function prevents future
extensions, for example exporting a second function from the same module
would mandate all modules that use the first function via require()
to
be updated to require().func1
, which may cause many headaches to developers.
module.exports.func1 = function () {
return /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
};
module.exports.func2 = function () { ... }
...
const { func1 } = require('name')
The recommendation is to always return functions or preferably classes
as properties of the module.exports
object, and get them individually by name.
Prefer static classes to group methods
Prepare your module to export multiple functions; group them (by functionality) either below a parent object, or, even better, in classes with static members.
The main advantage of this scheme is that adding new exports will only change the interface incrementally, minimising the risk to break backward compatibility.
Use the spread operator
Th spread operator expands an iterable into its member objects. It also works with arrays and objects
- Create array copies
const arr1 = [1, 2 3]
const arr2 = [...arr1]
- Concatenate arrays
const arr1 = [1, 2 3]
const arr2 = [4, 5 6]
const arr3 = [...arr1, ...arr2]
- Enumerate object properties
class Base {
constructor (args) {
super({
name: 'tree',
parent: null,
...args
})
// ...
}
Iterate over an Array
For very simple loops, (one-two lines), use forEach()
:
const iterable = [10, 20, 30];
iterable.forEach((value) => {
console.log(value)
})
Debug note: Visual Studio Code is able to step into the lambda function.
For more complex loops, use for ... of
.
let iterable = [10, 20, 30]
for (const value of iterable) {
console.log(value)
}
// 10
// 20
// 30
If the value needs to be changed, use let
:
let iterable = [10, 20, 30];
for (let value of iterable) {
value += 1
console.log(value)
}
// 11
// 21
// 31
If the last value of the variable is needed outside the loop, define the variable before the loop:
let value
for (value of iterable) {
// ...
}
console.log(value)
Do not use for ... in
since it iterates over the enumerable
properties, which include inherited properties (use .hasOwnProperty()
to filter them out).
Iterate over the keys of an Object
If the order is not important, iterate over the keys or the entries:
// array like object with random key ordering
var anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(anObj)); // ['2', '7', '100']
console.log(Object.entries(anObj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]
for (const [key, value] of Object.entries(anObj)) {
console.log(key, value)
}
// 2 b
// 7 c
// 100 a
Check if object is Array
Array.isArray([1, 2, 3]); // true
Check if object is String
isString (x) {
return Object.prototype.toString.call(x) === '[object String]'
}
Check if object
isObject (x) {
return typeof x === 'object' && !Array.isArray(x)
}
Please note that null
is also an object, and everything created with
new
is also an object, including:
new Boolean(true)
new Number(1)
new String('abc')
Use Map to guarantee the order of the values
Although maps can be conveniently implemented with regular objects, the specs do not guarantee the insert order to be preserved.
If the order is important, or if the object need to store other
properties too, use a Map
.
Make node exports/imports look like ES6 exports/imports
Assuming classes are preferred, the EC6 syntax for export/import would look like:
export class WscriptAvoider { ... }
...
import { WscriptAvoider } from 'wscript-avoider.js'
So, to stay close to this syntax, the recommendation is to preserve the
original module.exports
object, and add properties to it, preferably
classes, even if they have only static members.
To import them, the syntax uses the explicit class name:
const WscriptAvoider = require('wscript-avoider').WscriptAvoider
WscriptAvoider.quitIfWscript(appName)
Cases like import { WscriptAvoider as Xyz } from 'wscript-avoider.js'
would
be naturally represented as:
const Xyz = require('wscript-avoider').WscriptAvoider
Xyz.quitIfWscript(appName)
In case the class is not static, instantiate it as usual.
Call exactly the class method
If a method of a class is overridden, calling it from the base class actually calls the derived method (run-time polymorphism).
To ensure the local function, use the function prototype:
class Base {
constructor () {
Base.prototype.clear()
}
clear () {
// ...
}
}
Pack function parameters as objects
For functions with more than 1 parameter, pack them in an object:
class Base {
/**
* @param {Object} params The generic parameters object.
*/
f1 (params) {
assert(params, 'There must be params.')
assert(params.name)
console.log(params.name)
}
f2 () {
f1 ({
name: value1,
type: value2
})
}
}
Use copy/move constructors & support methods
Make the constructor accept a from
parameter, to use it as a source
when creating the object.
The default behaviour is to make a copy of the original object.
Add a doMoveAll: true
to instruct a move operation; be sure original
references are cleared (set to undefined).
Alternatively use copyFrom(from)
and possibly appendFrom(from)
.
Use a clear()
method to clear the object content (it might be
useful during tests).
Use a separate location for private variables
To reduce clutter, group the private variables below a _private
object.
No need to end the name of the variables with _
.
Use a separate location for cached variables
If the object uses local cached objects, group them below a _cache
object.
Initialise it to an empty object in the constructor an in the clear()
method.
No need to end the name of the variables with _
.
class Base {
constructor () {
this._cache = {}
}
clear () {
this._cache = {}
}
}
Self
When a reference to the static methods or variables is needed, to make
things explicit, prefer to define a Self
variable.
From within regular methods, use:
// Explicit uppercase, to be obvious when a static property/method is used.
const Self = this.constructor
From within other static methods, use:
// Explicit uppercase, to be obvious when a static property/method is used.
const Self = this
From Understanding ECMAScript 6
The main specifications to be followed are those of ES 6; they override all other older specifications and style guides.
Block bindings
Use const
by default, and only use let
when you know a variable’s value needs to change
Functions
Use default parameters.
const makeRequest = function (url, timeout = 2000, callback = function() {}) {
// the rest of the function
}
const add = function (first, second = getValue(first)) {
return first + second
}
Use rest parameters.
const pick = function (object, ...keys) {
let result = Object.create(null)
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]]
}
return result
}
The Function
constructor.
var add = new Function("first", 'second = first',
'return first + second')
console.log(add(1, 1)) // 2
console.log(add(1)) // 2
JavaScript has two different internal-only methods for functions: [[Call]]
and [[Construct]]
. When a function is called without new, the [[Call]]
method is executed, which executes the body of the function as it appears in the code. When a function is called with new, that’s when the [[Construct]]
method
is called. The [[Construct]]
method is responsible for creating a new object, called the instance, and then executing the function body with this set to the instance. Functions that have a [[Construct]]
method are called constructors.
const Person = function (name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('You must use new with Person.')
}
}
var person = new Person('Nicholas')
var notAPerson = Person.call(person, 'Michael') // works!
Block-level functions
'use strict'
if (true) {
console.log(typeof doSomething) // throws an error
let doSomething = function () {
// empty
}
doSomething()
}
console.log(typeof doSomething)
Arrow functions are functions defined with a new syntax that uses an arrow (=>
)
- No this, super, arguments, and new.target bindings The values of
this
,super
, arguments, and new.target inside the function are defined by the closest containing non-arrow function - Cannot be called with new Arrow functions do not have a [[Construct]] method and therefore cannot be used as constructors. Arrow functions throw an error when used with new.
- No prototype Because you can’t use new on an arrow function, there’s no need for a prototype. The prototype property of an arrow function doesn’t exist.
- Can’t change this The value of this inside the function can’t be changed. It remains the same throughout the entire life cycle of the function.
- No arguments object Because arrow functions have no arguments binding, you must rely on named and rest parameters to access function arguments.
- No duplicate named parameters Arrow functions cannot have duplicate named parameters in strict or non-strict mode, as opposed to non-arrow functions, which cannot have duplicate named parameters only in strict mode.
let sum = (num1, num2) => num1 + num2
// effectively equivalent to:
let sum = function(num1, num2) {
return num1 + num2
};
Enhanced Object Functionality
It is now possible to modify an object’s prototype after it has been created thanks to ECMAScript 6’s Object.setPrototypeOf()
method.
In addition, you can use the super
keyword to call methods on an object’s prototype. The this
binding inside a method invoked using super
is set up to automatically work with the current value of this
.
From npm's "funny" coding style
As the name implies, some of them are funny, but some are still useful.
Line length
Keep lines shorter than 80 characters
Indentation
Indentation is two spaces
Curly braces
Curly braces belong on the same line
const f = function () {
while (foo) {
bar()
}
}
Semicolons
Don't use semicolons, except when required; for example to prevent the expression from being interpreted as a function call or property access, respectively.
;(x || y).doSomething()
;[a, b, c].forEach(doSomething)
Comma first
Put the comma at the start of the next line, directly below the token that starts the list
const magicWords = [ 'abracadabra'
, 'gesundheit'
, 'ventrilo'
]
, spells = { 'fireball' : function () { setOnFire() }
, 'water' : function () { putOut() }
}
, a = 1
, b = 'abc'
, etc
, somethingElse
Quotes
Use single quotes for strings except to avoid escaping
const ok = 'String contains "double" quotes'
const alsoOk = "String contains 'single' quotes or apostrophe"
const paramOk = `Back quotes string with ${parameter}`
White spaces
Put a single space in front of (
for anything other than a function call
Functions
Use named functions.
- always create a new
Error
object with your message (new Error('msg')
) - logging is done using the
npmlog
utility
Case, naming, etc
- use
lowerCamelCase
for multi-word identifiers when they refer to objects, functions, methods, properties, or anything not specified in this section - use
UpperCamelCase
for class names (things that you'd pass to "new") - use
all-lower-hyphen-css-case
for multi-word filenames and config keys - use named functions, they make stack traces easier to follow
- use
CAPS_SNAKE_CASE
for constants, things that should never change and are rarely used
null, undefined, false
- boolean variables and functions should always be either
true
orfalse
- when something is intentionally missing or removed, set it to
null
- don't set things to
undefined
- boolean objects are verboten
Exceptions
For the native Node.js callback usage, never to ever ever throw anything. It's worse than useless. Just send the error message back as the first argument to the callback.
But for the modern ES 6 promise usage, exceptions are fine.
From Node.js modules
Modules
Modules are a way of preventing multiple JavaScript units to pollute the global namespace.
Objects defined at root level in a module are not global, but belong to the module; the usual name for this is module-global.
Caching
Modules are cached after the first time they are loaded. This means (among other things) that every call to require('foo') will get exactly the same object returned, if it would resolve to the same file. Multiple calls to require('foo')
will not cause the module code to be executed multiple times.
From this point of view, modules behave like singletons; they can also be compared to static classes in C++.
Leaving status at the module level can be either a blessing or a curse, depending on the environment used to run the module. In server environments, using module-global variables is like using static variables in a multi-threaded environment, if not handled correctly it may have unexpected results.
Exports
To make some functions and objects visible outside the module, you can add them as properties to the special modules.exports
object:
const PI = Math.PI
module.exports.area = (r) => PI * r * r
module.exports.circumference = (r) => 2 * PI * r
Although you can rewrite the module.export
to be a single function (such as a constructor), still prefer to add them as properties to the object and refer to them explicitly in the require()
line:
const square = require('./square.js').square
const mySquare = square(2)
console.log(`The area of my square is ${mySquare.area()}`)
module.exports.area = (width) => {
return {
area: () => width * width
}
}
If you want to export a complete object in one assignment instead of building it one property at a time, assign it to module.exports
.
Accessing the main module
When a file is run directly from Node.js, require.main
is set to its module. That means that you can determine whether a file has been run directly by testing
require.main === module
The module wrapper
Before a module's code is executed, Node.js will wrap it with a function wrapper that looks like the following:
module = ... // an object for the current module
module.export = {} // an empty object
exports = module.export // a reference to the exports; avoid using it
__filename = '/x/y/z/abc.js'
__dirname = '/x/y/z'
(function (exports, require, module, __filename, __dirname) {
// Your module code actually lives in here
});
In each module, the module
variable is a reference to the object representing the current module. module
isn't actually a global but rather local to each module.
The module.exports
object is created by the Module system. Sometimes this is not acceptable; many want their module to be an object of their own. To do this, assign the desired export object to module.exports
.
For convenience, module.exports
is also accessible via the exports
module-global. Note that assigning a value to exports
will simply rebind the local exports variable, which is probably not what you want to do; if the relationship between exports
and module.exports
seems like magic to you, ignore exports
and only use module.exports
.
Note that assignment to module.exports
must be done immediately. It cannot be done in any callbacks.
Global objects
These are really objects, available in all modules. (see Node.js Globals)
global
process
console
console.log('msg')
- writes 'msg' to stdoutconsole.warn('msg')
- writes 'msg' to stderrconsole.error('msg')
- writes 'Error: msg' to stderrconsole.assert(value, 'msg')
The Spread syntax
According to JavaScript ES6— The Spread Syntax (…):
- The spread syntax is simply three dots:
...
- It allows an iterable to expand in places where 0+ arguments are expected.
Useful cases:
- inserting Arrays
var mid = [3, 4];
var arr = [1, 2, ...mid, 5, 6];
console.log(arr);
- Math
var arr = [2, 4, 8, 6, 0];
var max = Math.max(...arr);
console.log(max);
- Copy an Array
var arr = ['a', 'b', 'c'];
var arr2 = [...arr];
console.log(arr2);
- String to Array
var str = "hello";
var chars = [...str];
console.log(chars);
Links to other style guides
Preferred:
Other links:
- 10 Best JavaScript Style Guides
- airbnb JavaScript Style Guide
- Idiomatic.js JavaScript Style Guide
- Google ES6 Style Guide
- Crockford Code Conventions for the JavaScript Programming Language
- felixge/node-style-guide
- RisingStack/node-style-guide
- kettanaito/naming-cheatsheet
Linting Tools
Preferred (used automatically by Standard):
- ESLint, but indirectly via the 'Standard JS' tools.
Other links: