6 JavaScript Do’s and Don’ts

The JavaScript language has undergone many changes since its inception. Therefore, it is not so easy to keep track of good practices now with so many new features, changes and frameworks.

In this article, we’ll look at some general rules that are best used in JavaScript. These tips will help us write better code. They are suitable for both beginners and experienced developers.


1. Declaring one variable on one line

In JavaScript, you can declare multiple variables on one line at once. This is something I would not recommend doing. Declaring variables on different lines makes the code easier to read and understand.

Let’s see a couple of examples:

// ❌ нежелательно
const x = 0, y = 10, z = 20;

// ✅ предпочтительно
const x = 0;
const y = 10;
const z = 20;

Declaring multiple variables on the same line may look “cooler”, but it’s impractical. The code becomes less readable. This approach can also lead to undesirable results.

In the example below, it might seem that x, yAnd z are all constants. But this is not true, the only constant is x.

const x = y = z = 10;

// ❌ y и z не константы, им можно присвоить новые значения
y = 15; // works
z = 15; // works

// только x константа
x = 15; // error: Uncaught TypeError: Assignment to constant variable.

But every rule has its exceptions. We can define more than one variable on the same line using destructuring.

Let’s look at the code:

const point = { x: 10, y: 15, z: 20 };

// ✅ желательно
const { x, y } = point;

// ✅ выведет 10
console.log(x);

// ✅ выведет 15
console.log(y);

The example above is the best solution.


2. Understanding browser optimization

Because JavaScript doesn’t compile, the only optimization the engine does is at runtime. He comes up with ways to optimize as he goes.

These methods are hidden and not always clear. Each engine has its own implementation of optimizations. For example, in the Chrome enginev8 this mechanism is called TurboFan. By understanding some of its internals, we can write more efficient code.

If we ignore the engine, performance may suffer. Our application will run slower, which will obviously not please the user.

Let’s look at a few situations where we can benefit:

Prototypes

The JavaScript engine understands that any changes to prototypes are quite rare. The object prototype is stable and predictable. This is why the engine does some optimizations in the early stages of prototypes.

However, there are two sides of the same coin. When the object’s prototype changes, the engine recalculates all optimizations. These changes make the application run slower. It can even slow down the code that interacts with the prototype.

Consider an example of bad usage:

// объявление прототипа
function Item() {}
Item.prototype.save = () => {};
Item.prototype.delete = () => {};

...
...

function foo() {
  // ❌ плохо, мы не должны менять прототип
  delete Item.prototype.save;
  // выведете undefined: save метод больше недоступен
  console.log(Item.prototype.save);
}

If you really need similar functionality, just use the properties of the object itself. This will not recalculate its prototype optimizations.

Let’s look at the corrected example:

// объявление прототипа
function Item() {
  this.save = () => {};
}
Item.prototype.delete = () => {};

...
...

function foo() {
  const newItem = new Item();
  // ✅ желательно, удаление свойства без изменения самого прототипа
  delete newItem.save;
  // выведет undefined: save метод больше недоступен
  console.log(newItem.save);
}

More can be found in the article from MDN.

Typing

Since JavaScript uses JIT compilation, it needs to do a lot of checks before executing any function. It largely depends on optimizations.

How do these optimizations happen? When a function is executed frequently, it “heats up”. The engine stores the compiled version. When a function gets “hot” it is sent as an optimizing compiler. There are many optimization strategies used.

One such strategy is typing. The function will create a stub for each combination of type and parameters. This means that if our function is monomorphic (with the same type of parameters), only one stub is required. If it is polymorphic, one stub will be required for each combination of parameter and type.

Accordingly, if you follow the types of parameters, you can improve performance.

Consider an example:

function add(a, b) {
  return a + b;
}

// ✅ "горячую" функцию JIT оптимизирует эффективно
add(1,2);
add(1,1);
add(2,3);
add(4,5);

If we were to execute this function with different types of parameters, the code would not run as fast.

Let’s just look at this option:

function sum(a, b) {
  return a + b;
}

// ❌ если "горячая" функция будет храниться
// будет создано много заглушек для каждой комбинации
add(1,2);
add(1,'3');
add(2,true);
add(4,5);

TypeScript, for example, can help us make our methods as efficient as possible.


3. Early return call

We are used to the pattern if/elseand did not question it. However, with experience, you might realize that using a full branch is:

How can we improve our code? We just use the early return pattern.

“Early call return” is a pattern in which it is recommended to return some result as early as possible without resorting to else expression.

Let’s take a look at the classic implementation FizzBuzz functions.

The code below could be a solution:

function FizzBuzz(i) {
    let result = undefined;
    if (i % 15 == 0) {
        result="FizzBuzz";
    } else if (i % 3 == 0) {
        result="Fizz";
    } else if (i % 5 == 0) {
        result="Buzz";
    } else {
        result = i;
    }
    return result;
}

However, it can be improved by using the early return pattern:

function FizzBuzz(i) {
    if (i % 15 == 0) {
        return 'FizzBuzz';
    }
    if (i % 3 == 0) {
        return 'Fizz';
    }
    return  (i % 5 == 0) ? 'Buzz' : i;
}

As a result, our code became:

  • more thoughtful

  • more readable

  • more efficient


4. Adopt functional programming

JavaScript is a multi-paradigm programming language. We can choose between object-oriented programming and functional programming. The first approach became more accessible with the advent of classes in ES6.

True, this is still syntactic sugar for regular prototypal inheritance in JS. This can lead to conflicts.

In my opinion, the functional approach is modular and very easy to test.

We can see how the creators of React have simplified the development process by abandoning the class approach. Even if you have never written in React, you can immediately notice a big difference.

Let’s look at an example with React classes:

class ClassComponent extends React.Component{
    constructor(){
        super();
        this.state={
            count :0
        };
    }

    increase = () => {
        this.setState({count : this.state.count + 1});
    }
 
    render(){
        return (
            <div>
               <p> {this.state.count}</p> 
               <button onClick={this.increase}> Add</button>
            </div>
        )
    }
}

Now let’s rewrite the same component using functional programming:

const functionComponent = () => {
    const [count, setCount] = useState(0);
    const increase = () => setState(count + 1);

    return (
        <div>
           <p> {count}</p> 
           <button onClick={increase}> Add</button>
        </div>
    );
}

As a result, we spend less code, which makes it easier.

The functional approach has many advantages. It saves us from a lot of the problems associated with changing entities.


5. Using ‘===’ to test for equality

Operator == is a comparison operator that casts operands to the same type.

It uses type casting to compare their values. For example, when a number and a string are compared, the engine will convert the string to a number and then compare them.

Operator == compares values, but not types.

Let’s see some examples:

'1' == 1 // ✅ true

true == 1 // ✅ true

false == 0 // ✅ true

'0' == false // ✅ true

This can lead to some “fun” situations, as shown in the picture below:

To prevent unwanted behavior, it’s better to check both the type and the value. This is possible with the operator === .

A strict comparison will first compare the value types. If they are not equal, then false will be returned. Only if they match will the values ​​themselves be checked.

⚠️ There is a feature that NaN equals nothing in a strict comparison. For this we can use isNan() .

Let’s look at the operator === In action:

1 === 1 // ✅ true
1 === '1' // ❌ false

const obj = {};

obj === obj // ✅ true
'x' === 'x' // ✅ true

NaN === NaN // ❌ false
isNaN(NaN) // ✅ true

6. Await instead of promises

Before promises, working with asynchronous operations was quite tedious. Everything was done through callbacks. This led to the so-called “callback hell”. The code was difficult to read and maintain. Promises have helped write better code, but they are still far from perfect and can lead to promise hell.

Async/await functionality was introduced in ES7. He simplified the work with promises in the language. Working with asynchrony has become more convenient, because all the hard work has been taken over by the engine. And the release of the top-level await in ES12 added the final piece missing from this functionality.

Now there are not many cases where we need to use promises instead of async/await. Usage async/await will improve the readability of our code.

Consider an example:

const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);

function addItem() {
    createItem()
        .then(({ id }) => updateStock(id))
        .then(() => console.log('success'))
        .catch(() => console.error('oops error'));
}

If we rewrite it with async/awaitthen it becomes easier to read:

const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);

async function addItem() {
    try {
      const { id } = await createItem();
      await updateStock(id);
      console.log('success');
    } catch {
      console.error('oops error');
    }
}

Async/await works well with new promise methods like Promise.all, Promise.any, Promise.allSettled etc.

Let’s look at an example with promises and async/await in ES12:

(async () => {
    const result = await Promise.any([
        Promise.reject('Error 1'),
        Promise.reject('Error 2'),
        Promise.resolve('success'),
    ]);
    console.log(`result: ${result}`);
})();
// result: success

Outcome

These have been my development notes when it comes to JavaScript. Of course, there are many more, but these six are my favorites. Importance of use Strict also very close to them. Luckily, we don’t have to worry too much about this, as there are tools that will already do everything for us.

Be sure to use ESLint. It automates the development cycle, helps us write better code, and speeds up the code review process.

Similar Posts

Leave a Reply