Writing clean and scalable JavaScript code: 12 tips

Original author: Lukas Gisder-Dubé
  • Transfer
  • Tutorial
JavaScript is from the early web. At first, simple scripts were written on it that “enlivened” the pages of sites. Now JS has become a full-fledged programming language that can even be used to develop server-side projects.

Modern web applications rely heavily on JavaScript. This is especially true for single-page applications (Single-Page Application, SPA). With the advent of libraries and frameworks such as React, Angular, and Vue, JavaScript has become one of the main building blocks of web applications.



Scaling such applications, whether it is about their client or server parts, can be a very difficult task. If such applications are based on a poorly thought out architecture, then their developers will sooner or later face certain limitations. They are drowning in a sea of ​​unpleasant surprises.

The author of the article, the translation of which we are publishing today, wants to share tips on writing pure JavaScript code. He says the article is for JS programmers of any skill level. But it will be especially useful for those who are familiar with JavaScript at least at an intermediate level.

1. Code isolation


In order to keep the project’s code base clean so that the code can be easily read, it is recommended that code fragments be separated into separate blocks based on their purpose. These blocks are usually functions. I consider this recommendation the most important one I can give. If you are writing a function, you must immediately focus on the fact that this function would be aimed at solving a single problem. The function should not be designed to solve several problems.

In addition, you should strive to ensure that function calls do not lead to side effects. In most cases, this means that the function should not change something that is declared outside of it. Data is received through parameters. She should not work with anything else. You need to return any of the functions using the keyword return.

2. Breakdown of the code into modules


Functions that are used in a similar way or perform similar actions can be grouped in one module (or, if you want, in a separate class). Suppose you need to do various calculations in your project. In such a situation, it makes sense to express the different stages of such calculations in the form of separate functions (isolated blocks), the calls of which can be combined into chains. However, all these functions can be declared in a single file (that is, in a module). Here is an example of a module calculation.jsthat contains similar functions:

function add(a, b) {
    return a + b   
}
function subtract(a, b) {
    return a - b   
}
module.exports = {
    add,
    subtract
}

And here is how you can use this module in another file (let's call it index.js):

const { add, subtract } = require('./calculations')
console.log(subtract(5, add(3, 2))

Frontend application developers can be given the following recommendations. To export the most important entities declared in the module, use the default export options. For secondary entities, you can use named export.

3. Use multiple function parameters instead of a single object with parameters


When declaring a function, you should strive to use several parameters, rather than a single object with parameters. Here are a couple of examples:

// Хорошо
function displayUser(firstName, lastName, age) {
    console.log(`This is ${firstName} ${lastName}. She is ${age} years old.`)
}
// Плохо
function displayUser(user) {
    console.log(`This is ${user.firstName} ${user.lastName}. She is ${user.age} years old.`)
}

The presence of a function with several parameters allows you to immediately find out what needs to be passed to it by looking at the first line of its declaration. This is precisely the reason why I give this recommendation.

Despite the fact that when developing functions, you need to strive to ensure that each of them solves only one problem, the size of the function code can be quite large. If a function accepts a single object with parameters, then in order to find out exactly what it expects, you may need to look at all its code, spending a lot of time on it. Sometimes it may seem that when working with functions it is much easier to use a single object with parameters. But if you write functions, given the possible future scaling of the application, it is better to use several parameters.

It should be noted that there is a certain limit after which the use of individual parameters loses its meaning. In my case, these are four to five parameters. If a function needs so much input, then a programmer should consider using an object with parameters.

The main reason for this recommendation is that the individual parameters expected by the function must be passed to it in a specific order. If some of the parameters are optional, then something like undefinedor must be passed to the function instead null. When using an object with parameters, the order of the parameters in the object does not matter. With this approach, you can do without setting the optional parameters c undefined.

4. Restructuring


Restructuring is a useful mechanism that appeared in ES6. It allows you to extract the specified fields from objects and immediately write them to variables. It can be used when working with objects and modules:

// Работа с модулем
const { add, subtract } = require('./calculations')

In particular, when working with modules, it makes sense to import into a file not the entire module, but only the necessary functions, giving them understandable names. Otherwise, you will have to access functions using a variable symbolizing the module.

A similar approach is applicable to those cases when a single object is used as a function parameter. This allows looking at the first line of the function to find out immediately what exactly it expects to receive as an object with parameters:

function logCountry({name, code, language, currency, population, continent}) {
    let msg = `The official language of ${name} `
    if(code) msg += `(${code}) `
    msg += `is ${language}. ${population} inhabitants pay in ${currency}.`
    if(contintent) msg += ` The country is located in ${continent}`
}
logCountry({
    name: 'Germany',
    code: 'DE',
    language 'german',
    currency: 'Euro',
    population: '82 Million',
})
logCountry({
    name: 'China',
    language 'mandarin',
    currency: 'Renminbi',
    population: '1.4 Billion',
    continent: 'Asia',
})

As you can see, despite the fact that the function accepts a single object with parameters, its destructuring allows you to find out what exactly needs to be placed in it when the function is called. The next tip will be on how to more accurately tell the user what the function expects.

By the way, restructuring can also be used when working with functional components of React.

5. Set default values ​​for function parameters


The standard values ​​of the parameters of functions, the default values ​​of parameters, it makes sense to use when destructuring objects with parameters, and in those cases when functions accept lists of parameters. Firstly, it gives the programmer an example of what functions can be passed. Secondly, it allows you to find out which parameters are mandatory and which are optional. We supplement the function declaration from the previous example with standard parameter values:

function logCountry({
    name = 'United States', 
    code, 
    language = 'English', 
    currency = 'USD', 
    population = '327 Million', 
    continent,
}) {
    let msg = `The official language of ${name} `
    if(code) msg += `(${code}) `
    msg += `is ${language}. ${population} inhabitants pay in ${currency}.`
    if(contintent) msg += ` The country is located in ${continent}`
}
logCountry({
    name: 'Germany',
    code: 'DE',
    language 'german',
    currency: 'Euro',
    population: '82 Million',
})
logCountry({
    name: 'China',
    language 'mandarin',
    currency: 'Renminbi',
    population: '1.4 Billion',
    continent: 'Asia',
})

It is obvious that in some cases, if a function parameter was not passed to it when calling a function, it is necessary to give an error, rather than using the standard value of this parameter. But often, however, the technique described here is very helpful.

6. Do not pass unnecessary data to functions


The previous recommendation leads us to one interesting conclusion. It consists in the fact that functions do not need to transfer the data that they do not need. If you follow this rule, then the development of functions may require additional time. But in the long run, this approach will lead to the formation of a code base that is distinguished by good readability. In addition, it is incredibly useful to know what kind of data is used in each particular place of the program.

7. Limiting the number of lines in files and the maximum level of code nesting


I have seen large files with program code. Very big. Some had over 3,000 lines. In such files it is very difficult to navigate.

As a result, it is recommended to limit the size of files measured in lines of code. I usually strive to ensure that the size of my files does not exceed 100 lines. Sometimes, when it’s difficult to break a certain logic into small fragments, the sizes of my files reach 200-300 lines. And very rarely, their size reaches 400 lines. Files that exceed this limit are hard to read and maintain.

In the course of work on your projects, boldly create new modules and folders. The project structure should resemble a forest consisting of trees (groups of modules and module files) and branches (sections of modules). Strive to ensure that your projects do not look like mountain ranges.

If we talk about the appearance of the files themselves with the code, then they should be similar to the terrain with low hills. It is about avoiding large levels of code nesting. It is worth striving to ensure that the nesting of the code does not exceed four levels.

Perhaps observing these recommendations will help to apply the appropriate ESLint linter rules.

8. Use tools to automatically format code


When working on JavaScript projects in a team, you need to develop a clear guide to the style and formatting of the code. You can automate code formatting with ESLint. This linter offers the developer a huge set of customizable rules. There is a team eslint --fixthat can fix some errors.

I, however, do not recommend using ESLint, and to automate code formatting Prettier . With this approach, the developer may not have to worry about code formatting. He only needs to write quality programs. All code that is automatically formatted using a single set of rules will look consistent.

9. Use well-designed variable names


The name of the variable should ideally reflect its contents. Here are some guidelines for selecting informative variable names.

▍ Functions


Usually functions perform some kind of action. People, when talking about actions, use verbs. For example - convert (convert) or display (show). Function names are recommended to be formed so that they begin with a verb. For example - convertCurrencyor displayUser.

▍Arrays


Arrays usually contain sets of some values. As a result, it makes sense to add a letter to the name of the variable that stores the array s. For instance:

const students = ['Eddie', 'Julia', 'Nathan', 'Theresa']

▍Logical values


Boolean variable names make sense to start with isor has. This brings them closer to the constructions that are available in ordinary language. For example, here is the question: “Is that person a teacher?”. The answer to this can be “Yes” or “No”. You can do the same by selecting names for logical variables:

const isTeacher = true // или false

▍ Parameters of functions passed to standard array methods


Here are a few standard methods of JavaScript arrays: forEach, map, reduce, filter. They allow you to perform certain actions with arrays. They are passed functions that describe operations on arrays. I saw how many programmers simply pass parameters with names like elor to such functions element. Although this approach frees the programmer from thinking about naming such parameters, it is better to call them based on the data that appears in them. For instance:

const cities = ['Berlin', 'San Francisco', 'Tel Aviv', 'Seoul']
cities.forEach(function(city) {
...
})

▍ Identifiers


It often happens that a programmer needs to work with the identifiers of certain data sets or objects. If such identifiers are nested, nothing special needs to be done with them. For example, when working with MongoDB, I usually convert it _idto the front-end application before returning the object id. When extracting identifiers from objects, it is recommended to form their names by setting the idtype of the object. For instance:

const studentId = student.id
// или
const { id: studentId } = student // деструктурирование с переименованием

An exception to this rule is working with MongoDB references in models. In such cases, it is recommended to name the fields in accordance with the models referenced in them. This, when filling out documents to which there are links in the fields, will allow to keep the code clean and uniform:

const StudentSchema = new Schema({
    teacher: {
        type: Schema.Types.ObjectId,
        ref: 'Teacher',
        required: true,
    },
    name: String,
    ...
})

10. Use the async / await construct where possible.


Using callbacks degrades code readability. This is especially true for nested callbacks. Promises straightened things out a bit, but I believe that code that uses the async / await construct is best read. Even beginners and developers who switched to JavaScript from other languages ​​can easily understand this code. The most important thing here is to master the concepts underlying async / await. Do not use this design everywhere because of its novelty.

11. The procedure for importing modules


Recommendations 1 and 2 demonstrated the importance of choosing the right place to store code to ensure it is supported. Similar ideas apply to the import order of modules. Namely, we are talking about the fact that the logical order of importing modules makes the code clearer. When importing modules, I follow the following simple scheme:

// Пакеты сторонних разработчиков
import React from 'react'
import styled from 'styled-components'
// Хранилища
import Store from '~/Store
// Компоненты, поддерживающие многократное использование
import Button from '~/components/Button'
// Вспомогательные функции
import { add, subtract } from '~/utils/calculate'
// Субмодули
import Intro from './Intro'
import Selector from './Selector'

This example is based on React. The same idea will not be difficult to transfer to any other development environment.

12. Avoid using console.log


The command console.logis a simple, fast and convenient tool for debugging programs. There are, of course, more advanced tools of this kind, but I think that console.logalmost all programmers still use it. If, using console.logfor debugging, you do not remove the calls of this command that become unnecessary in time, the console will soon become completely disordered. It should be noted that it makes sense to leave some logging teams even in the code of projects that are completely ready for work. For example, commands that display error messages and warnings.

As a result, we can say that for debugging purposes it is quite possible to useconsole.log, and in cases where the logging team is planned to be used in working projects, it makes sense to resort to specialized libraries. Among them are loglevel and winston . In addition, ESLint can be used to combat unnecessary logging commands. This allows a global search and removal of such commands.

Summary


The author of this material says that everything he talked about here helps him a lot in maintaining the cleanliness and scalability of the code base of his projects. We hope you find these tips useful.

Dear readers! What could you add to the 12 tips here for writing clean and scalable JS code?


Also popular now: