A mostly reasonable approach to JavaScript, inspired by the Airbnb JavaScript Style Guide.
The end-user experience always come first which means performance should always be top-of-mind. The JSPerf examples used in this guide do not use large datasets. If you find yourself working with large datasets and the suggested approach based on this guide performs slower, don't be afraid to push back on a per-project basis (see Performance vs Readability).
Use const
for all of your references; avoid using var
.
Why? This ensures that you can’t reassign your references, which can lead to bugs and difficult to comprehend code.
🚫 Nope. 🚫
var a = 1;
var b = 2;
🎉 Yep! 🎉
const a = 1;
const b = 2;
- ESLint:
If you must reassign references, use let
instead of var
.
Why?
let
is block-scoped rather than function-scoped likevar
. Function-scoped variables are hoisted which can lead to bugs if you are not careful. Using block-scoped variables makes our code more predictable by giving the variable an explicit scope.
🚫 Nope. 🚫
var count = 1;
if (true) {
count += 1;
}
🎉 Yep! 🎉
let count = 1;
if (true) {
count += 1;
}
- ESLint: no-var
Note that both let
and const
are block-scoped.
// Both `const` and `let` only exist in the blocks they are defined in.
{
let a = 1;
const b = 1;
}
console.log(a); // ReferenceError: a is not defined
console.log(b); // ReferenceError: b is not defined
Use the literal syntax for object creation.
Why? While there are no performance differences between the two approaches, the byte savings and conciseness of the object literal form is what has made it the de facto way of creating new objects.
🚫 Nope. 🚫
const item = new Object();
🎉 Yep! 🎉
const item = {};
- ESLint: no-new-object
Use object shorthand syntax for both methods and property values.
Why? ECMAScript 6 provides a concise form for defining object literal methods and properties. This syntax can make defining complex object literals much cleaner.
🚫 Nope. 🚫
const atom = {
value: 1,
addValue: function (value) {
return atom.value + value;
},
};
🎉 Yep! 🎉
const atom = {
value: 1,
addValue(value) {
return atom.value + value;
},
};
🚫 Nope. 🚫
const generalLeiaOrgana = 'General Leia Organa';
const obj = {
generalLeiaOrgana: generalLeiaOrgana,
};
🎉 Yep! 🎉
const generalLeiaOrgana = 'General Leia Organa';
const obj = {
generalLeiaOrgana,
};
- ESLint: object-shorthand
Only quote properties that are invalid identifiers.
Why? In general we consider it subjectively easier to read. It improves syntax highlighting, and is also more easily optimized by many JS engines.
🚫 Nope. 🚫
const obj = {
'foo': 3,
'bar': 4,
'data-blah': 5,
};
🎉 Yep! 🎉
const obj = {
foo: 3,
bar: 4,
'data-blah': 5,
};
- ESLint: quote-props
Do not call Object.prototype
methods directly, such as hasOwnProperty
, propertyIsEnumerable
, and isPrototypeOf
.
Why? In ECMAScript 5.1,
Object.create
was added, which enables the creation of objects with a specified[[Prototype]]
.Object.create(null)
is a common pattern used to create objects that will be used as a Map. This can lead to errors when it is assumed that objects will have properties fromObject.prototype
.
🚫 Nope. 🚫
console.log(object.hasOwnProperty(key));
🎉 Yep! 🎉
// Good
console.log(Object.prototype.hasOwnProperty.call(object, key));
// Best
const has = Object.prototype.hasOwnProperty; // cache the lookup once, in module scope.
console.log(has.call(object, key));
- ESLint: no-prototype-builtins
Prefer the object spread operator over Object.assign
to shallow-copy objects. Use the object rest operator to get a new object with certain properties omitted.
Why? Object spread is a declarative alternative which may perform better than the more dynamic, imperative Object.assign.
🚫 Nope. 🚫
// This mutates `original` ಠ_ಠ
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 });
delete copy.a; // So does this!
// Works but not preferred
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }
🎉 Yep! 🎉
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }
const { a, ...noA } = copy; // noA => { b: 2, c: 3 }
- ESLint: prefer-object-spread
- JSPerf: Shallow Copy Objects
Use the literal syntax for array creation.
Why? Use of the
Array
constructor to construct a new array is generally discouraged in favor of array literal notation because of the single-argument pitfall and because theArray
global may be redefined.
🚫 Nope. 🚫
const items = new Array();
🎉 Yep! 🎉
const items = [];
- ESLint: no-array-constructor
Use Array.prototype.push() instead of direct assignment to add items to an array.
🚫 Nope. 🚫
const someStack = [];
someStack[someStack.length] = 'abracadabra';
🎉 Yep! 🎉
const someStack = [];
someStack.push('abracadabra');
- JSPerf: Adding Array Items
Use array spread syntax ...
to shallow-copy arrays.
Why? Better overall performance.
🚫 Nope. 🚫
// Too slow
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
const len = animals.length;
const animalsCopy = [];
let i;
for (i = 0; i < len; i ++) {
animalsCopy[i] = animals[i];
}
// Works but is not preferred
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
const animalsCopy = animals.slice();
🎉 Yep! 🎉
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
const animalsCopy = [...animals];
- JSPerf: Shallow Copy Arrays
To convert an iterable object (e.g. NodeList) to an array, use array spread syntax ...
instead of Array.from.
Why? Better performance.
🚫 Nope. 🚫
const paragraphs = document.querySelectorAll('p');
const nodes = Array.from(paragraphs);
🎉 Yep! 🎉
const paragraphs = document.querySelectorAll('p');
const nodes = [...paragraphs];
If using Babel with @babel/preset-env
with option loose:true
, and are transpiling to older targets in a .browserlistrc
, you may need to add the following to your Babel config (e.g., babel.config.js
):
plugins: [
...,
'@babel/plugin-transform-spread',
...
]
(The default option for this plugin is loose:false
, which will override the global setting)
- ESLint: prefer-spread
- JSPerf: Arrays From Iterables
Use Array.from
for converting an array-like object to an array.
Why? Not only is it easier to read/type but it also performs better.
🚫 Nope. 🚫
const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 };
const arr = Array.prototype.slice.call(arrLike);
🎉 Yep! 🎉
const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 };
const arr = Array.from(arrLike);
- JSPerf: Arrays from Array-Like Objects
Use array spread syntax ...
instead of Array.from
for mapping over iterables.
Why? Overall better performance.
🚫 Nope. 🚫
const iterable = 'Hello there!';
const upperCase = letter => letter.toUpperCase();
const upperCaseLetters = Array.from(iterable, upperCase);
🎉 Yep! 🎉
const iterable = 'Hello there!';
const upperCase = letter => letter.toUpperCase();
const upperCaseLetters = [...iterable].map(upperCase);
- JSPerf: Mapping Over Iterables
Use return
statements in array method callbacks. It’s okay to omit the return
if the function body consists of a single statement returning an expression without side effects.
🚫 Nope. 🚫
inbox.filter(msg => {
const { subject, author } = msg;
if (subject === 'Mockingbird') {
return author === 'Harper Lee';
} else {
return false;
}
});
🎉 Yep! 🎉
inbox.filter(msg => {
const { subject, author } = msg;
if (subject === 'Mockingbird') {
return author === 'Harper Lee';
}
return false;
});
🎉 Also good! 🎉
[1, 2, 3].map((x) => {
const y = x + 1;
return x * y;
});
// The return can be omitted here.
[1, 2, 3].map(x => x + 1);
- ESLint: array-callback-return
Use object destructuring when accessing and using multiple properties of an object.
Why? Destructuring saves you from creating temporary references for those properties.
🚫 Nope. 🚫
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;
return `${firstName} ${lastName}`;
}
🎉 Yep! 🎉
// Good
function getFullName(user) {
const { firstName, lastName } = user;
return `${firstName} ${lastName}`;
}
// Best
function getFullName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}
- ESLint: prefer-destructuring
- JSPerf: Object Destructuring vs Not
- MDN Web Docs: Object Destructuring
How you destructure an array depends on your situation. Below are a couple of ways to complete the same task.
// This works!
const arr = [1, 2, 3, 4];
const first = arr[0];
const second = arr[1];
const rest = arr.slice(2);
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4]
// This works great also!
const arr = [1, 2, 3, 4];
const [first, second, ...rest] = arr;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4]
Note: For performance reasons, strongly consider use of the @babel/plugin-transform-destructuring plugin when using array destructuring.
- Babel Plugin: @babel/plugin-transform-destructuring
- JSPerf: Array Destructuring vs Not
- MDN Web Docs: Array Destructuring
Use object destructuring for multiple return values, not array destructuring.
Why? You can add new properties over time or change the order of things without breaking call sites.
🚫 Nope. 🚫
function processInput(input) {
return [left, right, top, bottom];
}
// the caller needs to think about the order of return data
const [left, __, top] = processInput(input);
🎉 Yep! 🎉
function processInput(input) {
return { left, right, top, bottom };
}
// the caller selects only the data they need
const { left, top } = processInput(input);
Use single quotes ''
for strings. The exception is if a string includes a literal '
single quote, use double quotes "
instead.
🚫 Nope. 🚫
// Should be single quote.
const name = "Cloud Four";
// Template literals should contain interpolation or newlines.
const name = `Cloud Four`;
// This string has a literal single quote!
const foo = 'What\'s for dinner?';
🎉 Yep! 🎉
const name = 'Cloud Four';
// It's okay to use double quotes here.
const foo = "What's for dinner?";
- ESLint: quotes
When programmatically building up strings, use template literals instead of concatenation.
Why? Template literals (template strings) give you a readable, concise syntax with proper newlines and string interpolation features.
🚫 Nope. 🚫
function sayHi(name) {
return 'How are you, ' + name + '?';
}
function sayHi(name) {
return ['How are you, ', name, '?'].join();
}
🎉 Yep! 🎉
function sayHi(name) {
return `How are you, ${name}?`;
}
- ESLint: prefer-template
Never use eval()
on a string, it opens too many vulnerabilities.
- ESLint: no-eval
Although it is possible to call functions before they are defined via hoisting we prefer to avoid this pattern in our code as it can be confusing.
Although this code works properly it may be more confusing and we generally avoid it:
logFoo();
function logFoo() {
console.log("foo");
}
We would prefer one of the following patterns:
// Define the function before calling
function logFoo() {
console.log("foo");
}
logFoo();
// Import the function
import {logFoo} from './log-foo.js';
logFoo();
In files that call a number of helper functions it can be helpful to move those functions to modules, or start the file with a main function that provides a summary of the steps taken in the file. For example:
// The `main` function contains an overview of the file's logic.
// (`main` could be switched to a more meaningful name in context.)
function main() {
thing1();
thing2();
thing3();
thing4();
}
function thing1() {}
function thing2() {}
function thing3() {}
function thing4() {}
main();
Another option is to move helper functions to modules:
// Import helpers so they're defined up front
import {thing1, thing2, thing3, thing4} from './helpers.js';
thing1();
thing2();
thing3();
thing4();
Never name a function parameter arguments
.
Why? This will take precedence over the
arguments
object that is given to every function scope.
🚫 Nope. 🚫
function foo(name, options, arguments) {
// ...
}
🎉 Yep! 🎉
function foo(name, options, args) {
// ...
}
Use the rest syntax ...args
instead of the arguments
object.
Why? Rest arguments are a real Array, and not merely Array-like as the
arguments
object is.
🚫 Nope. 🚫
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// Slow performance
function concatenateAll() {
const args = Array.from(arguments);
return args.join('');
}
🎉 Yep! 🎉
function concatenateAll(...args) {
return args.join('');
}
- ESLint: prefer-rest-params
Use function default parameter syntax rather than mutating function arguments.
🚫 Nope. 🚫
function doThings(opts) {
// If opts is falsey it can introduce bugs.
opts = opts || {};
// ...
}
function doThings(opts) {
if (opts === undefined) {
opts = {};
}
// ...
}
🎉 Yep! 🎉
function doThings(opts = {}) {
// ...
}
Avoid side effects with function default parameters.
Why? They are confusing to reason about.
🚫 Nope. 🚫
let b = 1;
// Eek!
function count(a = b++) {
console.log(a);
}
count(); // 1
count(); // 2
count(3); // 3
count(); // 3
Never use the Function
constructor to create a new function.
Why? Creating a function in this way evaluates a string similarly to
eval()
, which opens vulnerabilities.
🚫 Nope. 🚫
var add = new Function('a', 'b', 'return a + b');
var subtract = Function('a', 'b', 'return a - b');
🎉 Yep! 🎉
var x = function (a, b) {
return a + b;
};
- ESLint: no-new-func
Never mutate function parameters.
Why? Manipulating objects passed in as parameters can cause unwanted variable side effects in the original caller.
🚫 Nope. 🚫
function foo(bar) {
bar = 13;
}
function foo(bar) {
bar++;
}
🎉 Yep! 🎉
function foo(bar) {
var baz = bar;
}
- ESLint: no-param-reassign
Prefer the use of the spread syntax operator ...
to call variadic functions (a function that accepts a variable number of arguments).
Why? It’s cleaner, you don’t need to supply a context, and it's easier to compose
new
when compared to usingapply
.
🚫 Nope. 🚫
const args = [1, 2, 3, 4];
Math.max.apply(Math, args);
new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]));
🎉 Yep! 🎉
const args = [1, 2, 3, 4];
Math.max(...args);
new Date(...[2016, 8, 5]);
- ESLint: prefer-spread
- JSPerf: Spread Syntax for Variadic Functions
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...
TBD...