Guidelines for writing clean JavaScript code

Original author: Milos Protic
  • Transfer
If you care about the code itself, and how it is written, and are not only concerned with creating working programs, this means that you want your code to be clean. A professional developer writes code not only for computers, but also for himself who met this code in the future, and for other programmers. The code you write does not disappear forever in the bowels of the computer. It lives, changes, and if it is written poorly, it may very upset someone who will have to edit it after you write it. It is possible that you will be this “someone”. Based on these ideas, clean code can be defined as code written in such a way that it explains itself. This code can be easily understood by people, it will be easy to modify or extend it.





Code and WTF questions


The essence of WTF questions, like “WTF is that?” Comes down to extreme surprise and resentment. In Russian, these feelings can be expressed by the question “What the hell?”. “Damn”, depending on the situation, may well give way to something completely unprintable. How many times have you ever added someone’s code and ask similar questions?

When asking themselves WTF questions about someone else’s code, programmers ask themselves what it is (WTF is that?), What the author of the code tried to do (WTF did you do here?), Why is this or that construct present in the code (WTF is this for?)

Here is a picture according to which the only reliable indicator of code quality is the number of WTF questions per minute.


On the left is good code. To the right, it’s bad.

But seriously, in order to help you think about pure code, we will quote Robert Martin, known as Uncle Bob: “Even bad program code can work. However, if the code is not “clean,” it will always interfere with the development of the project. ”

Now let's look at some practical guidelines for writing clean code. We will use JavaScript here, but these recommendations can be applied to development in other languages.

1. Strict equality checks


Strive to ==use instead ===.

// Если необдуманно использовать оператор == - это может серьёзно повлиять на программную логику. Это можно сравнить с тем, что некто ожидает, что пойдёт налево, но по какой-то причине вдруг идёт направо.
0 == false // true
0 === false // false
2 == "2" // true
2 === "2" // false
// пример
const value = "500";
if (value === 500) {
  console.log(value);
  // этот код не выполнится
}
if (value === "500") {
  console.log(value);
  // этот код выполнится
}

2. Variables


Name the variables so that their names would reveal their essence, their role in the program. With this approach, it will be convenient to search for them in the code, and anyone who sees this code can more easily understand the meaning of the actions they perform.
Poorly:

let daysSLV = 10;
let y = new Date().getFullYear();
let ok;
if (user.age > 30) {
  ok = true;
}

Good:

const MAX_AGE = 30;
let daysSinceLastVisit = 10;
let currentYear = new Date().getFullYear();
...
const isUserOlderThanAllowed = user.age > MAX_AGE;

No need to add additional words to the variable names that are not needed.

Poorly:

let nameValue;
let theProduct;

Good:

let name;
let product;

You should not force someone who reads the code to have to remember the environment in which the variable is declared.

Poorly:

const users = ["John", "Marco", "Peter"];
users.forEach(u => {
  doSomething();
  doSomethingElse();
  // ...
  // ...
  // ...
  // ...
  // Тут перед нами ситуация, в которой возникает WTF-вопрос "Для чего используется `u`?"
  register(u);
});

Good:

const users = ["John", "Marco", "Peter"];
users.forEach(user => {
  doSomething();
  doSomethingElse();
  // ...
  // ...
  // ...
  // ...
  register(user);
});

You do not need to supply variable names with redundant information about the context in which they are used.

Poorly:

const user = {
  userName: "John",
  userSurname: "Doe",
  userAge: "28"
};
...
user.userName;

Good:

const user = {
  name: "John",
  surname: "Doe",
  age: "28"
};
...
user.name;

3. Functions


Use long descriptive names for functions. Given that a function is a description of a certain action, its name should be a verb or phrase that fully describes the essence of the function. The names of the arguments must be selected so that they adequately describe the data they represent. Function names should tell the code reader what exactly these functions do.

Poorly:

function notif(user) {
  // реализация
}

Good:

function notifyUser(emailAddress) {
  // реализация
}

Avoid using long lists of arguments. Ideally, functions should have two or fewer arguments. The less arguments a function has, the easier it will be to test it.

Poorly:

function getUsers(fields, fromDate, toDate) {
  // реализация
}

Good:

function getUsers({ fields, fromDate, toDate }) {
  // реализация
}
getUsers({
  fields: ['name', 'surname', 'email'],
  fromDate: '2019-01-01',
  toDate: '2019-01-18'
})

Use the default arguments, giving them preference over conditional constructs.

Poorly:

function createShape(type) {
  const shapeType = type || "cube";
  // ...
}

Good:

function createShape(type = "cube") {
  // ...
}

A function should solve one problem. Strive to ensure that one function does not perform many actions.

Poorly:

function notifyUsers(users) {
  users.forEach(user => {
    const userRecord = database.lookup(user);
    if (userRecord.isVerified()) {
      notify(user);
    }
  });
}

Good:

function notifyVerifiedUsers(users) {
  users.filter(isUserVerified).forEach(notify);
}
function isUserVerified(user) {
  const userRecord = database.lookup(user);
  return userRecord.isVerified();
}

Use Object.assignto set object properties by default.

Poorly:

const shapeConfig = {
  type: "cube",
  width: 200,
  height: null
};
function createShape(config) {
  config.type = config.type || "cube";
  config.width = config.width || 250;
  config.height = config. height || 250;
}
createShape(shapeConfig);

Good:

const shapeConfig = {
  type: "cube",
  width: 200
  // Значение ключа 'height' не задано
};
function createShape(config) {
  config = Object.assign(
    {
      type: "cube",
      width: 250,
      height: 250
    },
    config
  );
  ...
}
createShape(shapeConfig);

Do not use flags as parameters. Their use means that the function performs more actions than it should have performed.

Poorly:

function createFile(name, isPublic) {
  if (isPublic) {
    fs.create(`./public/${name}`);
  } else {
    fs.create(name);
  }
}

Good:

function createFile(name) {
  fs.create(name);
}
function createPublicFile(name) {
  createFile(`./public/${name}`);
}

Do not pollute the global scope. If you need to expand an existing object, use ES classes and inheritance mechanisms instead of creating functions in the prototype chain of standard objects.

Poorly:

Array.prototype.myFunc = function myFunc() {
  // реализация
};

Good:

class SuperArray extends Array {
  myFunc() {
    // реализация
  }
}

4. Conditional constructions


Try not to name the boolean variables so that there is negation in their names. The same goes for functions that return boolean values. Using such entities in conditional constructs makes it difficult to read code.

Poorly:

function isUserNotBlocked(user) {
  // реализация
}
if (!isUserNotBlocked(user)) {
  // реализация
}

Good:

function isUserBlocked(user) {
  // реализация
}
if (isUserBlocked(user)) {
  // реализация
}

Use the short form for conditional constructions. Perhaps this recommendation may seem trivial, but it is worth mentioning. Use this approach only for logical variables, and in the event that you are sure that the value of the variable will not be undefinedor null.

Poorly:

if (isValid === true) {
  // что-то сделать...
}
if (isValid === false) {
  // что-то сделать...
}

Good:

if (isValid) {
  // что-то сделать...
}
if (!isValid) {
  // что-то сделать...
}

Avoid logical constructions wherever possible. Use polymorphism and inheritance instead.

Poorly:

class Car {
  // ...
  getMaximumSpeed() {
    switch (this.type) {
      case "Ford":
        return this.someFactor() + this.anotherFactor();
      case "Mazda":
        return this.someFactor();
      case "McLaren":
        return this.someFactor() - this.anotherFactor();
    }
  }
}

Good:

class Car {
  // ...
}
class Ford extends Car {
  // ...
  getMaximumSpeed() {
    return this.someFactor() + this.anotherFactor();
  }
}
class Mazda extends Car {
  // ...
  getMaximumSpeed() {
    return this.someFactor();
  }
}
class McLaren extends Car {
  // ...
  getMaximumSpeed() {
    return this.someFactor() - this.anotherFactor();
  }
}

5. ES classes


Classes appeared in JavaScript relatively recently. They can be called syntactic sugar. What happens when using classes is based on prototypes of objects, as before. But the code that uses the classes looks different. In general, if possible, ES classes should be preferred over regular constructor functions.

Poorly:

const Person = function(name) {
  if (!(this instanceof Person)) {
    throw new Error("Instantiate Person with `new` keyword");
  }
  this.name = name;
};
Person.prototype.sayHello = function sayHello() { /**/ };
const Student = function(name, school) {
  if (!(this instanceof Student)) {
    throw new Error("Instantiate Student with `new` keyword");
  }
  Person.call(this, name);
  this.school = school;
};
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.printSchoolName = function printSchoolName() { /**/ };

Good:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    /* ... */
  }
}
class Student extends Person {
  constructor(name, school) {
    super(name);
    this.school = school;
  }
  printSchoolName() {
    /* ... */
  }
}

Organize methods so that they can be chained. Many libraries use this pattern, such as jQuery and Lodash. As a result, your code will be more compact than without using this pattern. The point is that at the end of each function of the class you need to return this. This will allow you to combine calls of such functions into chains.

Poorly:

class Person {
  constructor(name) {
    this.name = name;
  }
  setSurname(surname) {
    this.surname = surname;
  }
  setAge(age) {
    this.age = age;
  }
  save() {
    console.log(this.name, this.surname, this.age);
  }
}
const person = new Person("John");
person.setSurname("Doe");
person.setAge(29);
person.save();

Good:

class Person {
  constructor(name) {
    this.name = name;
  }
  setSurname(surname) {
    this.surname = surname;
    // Возвратим this для получения возможности объединять вызовы методов в цепочки
    return this;
  }
  setAge(age) {
    this.age = age;
    // Возвратим this для получения возможности объединять вызовы методов в цепочки
    return this;
  }
  save() {
    console.log(this.name, this.surname, this.age);
    // Возвратим this для получения возможности объединять вызовы методов в цепочки
    return this;
  }
}
const person = new Person("John")
    .setSurname("Doe")
    .setAge(29)
    .save();

6. What is better not to do


Anyone who wants his code to be clean should try not to repeat it. The point is to avoid situations in which you have to write the same code. In addition, you do not need to leave unused functions and program fragments that are never executed in the code base.

Duplicate code may appear for various reasons. For example, a project may have a pair of slightly different functions. The nature of these differences or lack of time forces the programmer, for example, to create two independent functions containing almost identical code. In such a situation, getting rid of duplicate code is to abstract the differences and to work with them at a higher level.

Now let's talk about unused code. This is code that is present in the codebase but does absolutely nothing. This happens, for example, when at a certain stage of development it is decided that there is no longer any sense in a certain fragment of the program. In order to get rid of such code fragments, you need to carefully review the code base and remove them. It is easiest to get rid of such code at the moment when it is decided that it is no longer needed. Later, you can forget about what it was used for. This will greatly complicate the fight with him.

If you put off the fight against unnecessary code, the program will resemble what is shown in the following figure.


Sometimes my code looks like this balcony. I don’t know what task he is solving, but I’m afraid to get rid of him.

Summary


Here we have discussed only a small part of the actions that can be taken to improve the code. The author of this material believes that the principles discussed here are often forgotten. Programmers try to follow them, but, for various reasons, do not always succeed in this. Perhaps, at the very beginning of the project, everyone remembers the importance of clean code, as a result, the program is neat. Then, as the deadlines get closer, they often forget about the purity of the code, paying attention only to what is marked as TODO or REFACTOR. At such times, the project customer will insist that the project be completed on time, and not that its code be clean.

Quite often, we publish materials on the problem of writing high-quality JavaScript code. If you are interested in this topic, here are a few links.


We hope that what you learned by reading this article and what you can find in other publications will help you in your quest to write clean JavaScript code.

Dear readers! Have you ever asked WTF questions while reading someone else's code?




Also popular now: