lbry.tech/documents/guides/js.md
ポール ウェッブ 347c89755b JS styleguide!
2018-08-29 16:57:57 -05:00

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

  1. Modules
  2. Strict Mode
  3. Spacing
  4. Semicolons
  5. Linting
  6. Strings
  7. Variable Declaration
  8. Conditionals
  9. Equality
  10. Ternary Operators
  11. Functions
  12. Prototypes
  13. Object Literals
  14. Array Literals
  15. Regular Expressions
  16. Comments
  17. Variable Naming
  18. 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;
}