Guidelines
Sequencing

Sequencing

Prefer array methods over loops

Array methods are functions that can be called on arrays to perform a specific operation, such as filter, map, or reduce the elements of the array. They are generally more concise and easier to read than loops, and they can often be composed with other array methods or functions to create more complex behavior.

Loops, on the other hand, are a control structure that allows you to iterate over a sequence of elements and perform a set of instructions for each element. There are several types of loops in JavaScript, including for, for...in, while, and do...while loops. Loops can be more flexible than array methods in certain situations, but they can also be more verbose and more prone to errors.

const numbers = [1, 2, 3, 4, 5];
 
// Using the map method to square each number
const squaredNumbers = numbers.map((number) => number * number);
 
console.log(squaredNumbers); // [1, 4, 9, 16, 25]
 
// Using the filter method to get only even numbers
const evenNumbers = numbers.filter((number) => number % 2 === 0);
 
console.log(evenNumbers); // [2, 4]
 
// Using the reduce method to sum all numbers
const sum = numbers.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  0
);
 
console.log(sum); // 15

Avoid mutable array methods

Mutable array methods should be avoid because they modify the original array, which can lead to unintended consequences if the array is being shared or accessed by multiple parts of the code.

There are several methods available on the Array object that allow you to modify the contents of an array.

  • pop(): removes the last element from an array and returns that element.
  • push(): adds one or more elements to the end of an array and returns the new length of the array.
  • shift(): removes the first element from an array and returns that element.
  • unshift(): adds one or more elements to the beginning of an array and returns the new length of the array.
  • splice(): removes elements from an array and/or adds new elements in their place.
  • reverse(): reverses the order of the elements in an array and returns the reversed array.
  • sort(): sorts the elements of an array in place and returns the sorted array.

Avoid infinite loop

An infinite loop in a program can cause it to consume a large amount of resources, such as memory and CPU time, and may cause the program to become unresponsive or crash.

An infinite loop can occur when a condition for exiting the loop is never met, or when the loop is improperly designed such that the condition for exiting the loop is never reached. There are several ways in which an infinite loop can occur in JavaScript.

  • Using a while or for loop without a proper stopping condition:

    while (true) {
      console.log("This will run forever!");
    }
     
    for (;;) {
      console.log("This will also run forever!");
    }
  • Accidentally modifying the loop counter inside the loop body:

    for (let i = 0; i < 10; i++) {
      console.log(i);
      i++; // this will cause the loop to run forever
    }
  • Using the continue or break statements incorrectly:

    for (let i = 0; i < 10; i++) {
      if (i % 2 === 0) {
        continue; // this will cause the loop to skip the odd numbers, running forever
      }
      console.log(i);
    }
     
    while (true) {
      console.log("This will run forever!");
      break; // this break statement will never be reached
    }
  • Modifying the loop condition inside the loop body:

    let i = 0;
    while (i < 10) {
      console.log(i);
      i++;
      if (i % 5 === 0) {
        i = 0; // this will cause the loop to start over, running forever
      }
    }
  • Not including the yield operator inside an infinite loop in a generator:

    function* numberGenerator(start) {
      let n = start;
      while (true) {
        console.log(n);
        // the missing "yield" will cause the loop to run forever
        n++;
      }
    }
     
    const generator = numberGenerator(10);
    generator.next();

To avoid infinite loops, make sure to carefully design the loop conditions and counter updates, use the break and continue statements or the yield operator correctly.

Use tail call optimization on recursion

Tail call optimization is a technique that allows a recursive function to return the result of another recursive call without adding a new stack frame. This can help avoid stack overflow by allowing the function to be executed in a single pass. The key to tail call optimization is to ensure that the recursive function call is the very last thing that happens in the function.

However, JavaScript does not currently have native support for optimizing tail-recursive functions. There are some workarounds and techniques that can be used to implement tail recursion in JavaScript, such as using a trampoline.

A trampoline is a function that repeatedly calls a recursive function until it returns a final result. This can help avoid stack overflow by allowing the function to be executed in a series of small steps instead of all at once.

const trampoline = (fn) => {
  let result = fn;
  while (typeof result === "function") {
    result = result();
  }
  return result;
};
 
const factorial = (n, total = 1) => {
  if (n === 1) {
    return total;
  }
  return () => factorial(n - 1, n * total);
};
 
console.log(trampoline(factorial(10000)));

Use recursion sparingly

Recursion can be a useful tool for certain types of problems, but it can also have some drawbacks. One of the main drawbacks of using recursion is that it can be less efficient than using an iterative approach. Recursive functions require the creation of a new stack frame for each recursive call, which can add overhead and increase the memory usage of the program.

There are no strict guidelines for when to use recursion, but here are some considerations to help you make the decision.

  • Use recursion when the problem can be naturally divided into smaller, self-similar subproblems that can be solved recursively. For example, calculating the factorial of a number or traversing a tree or graph data structure.

  • Use iteration when the problem can be naturally solved by repeatedly applying the same operations to the input until a certain condition is met. For example, summing the elements of an array or searching for an element in a sorted array.

Use transduction sparingly

A transducer is a pattern for creating composable, high-performance data transformation functions in functional programming. A transducer is a higher-order function that takes a transformation function as input and returns a new function that performs the transformation in a way that is optimized for efficient composition with other transducers.

Transducers are efficient because they can perform data transformations in a lazy, streaming manner, rather than having to materialize the entire input or output data structure in memory.

Transducers are particularly useful in situations where you need to transform large streams of data, such as when working with large collections or processing data as it comes in from an external source. They can be used to perform a wide range of transformations, such as filtering, mapping, reducing, and more.

Here is an example of using transducers to process a large stream of data:

const fs = require("fs");
const through2 = require("through2");
 
// define a transducer that filters out lines that don't contain a certain string
const filterLines = (string) => (step) => (accumulator, value) =>
  value.includes(string) ? step(accumulator, value) : accumulator;
 
// define a transducer that transforms each line to uppercase
const toUppercase = (step) => (accumulator, value) =>
  step(accumulator, value.toUpperCase());
 
// create a stream transformer using the transducers and through2
const transformer = through2.obj(
  filterLines("ERROR")(toUppercase((data, enc, cb) => cb(null, data)))
);
 
// read a large log file and transform the lines in a streaming manner
fs.createReadStream("large-log-file.log")
  .pipe(transformer)
  .on("data", (line) => console.log(line))
  .on("end", () => console.log("Done!"));

However, transducers can be more complex to understand and use than traditional loops or array methods and they can also be slower if they are not used appropriately. In particular, transducers can be slower when working with small data sets or when the overhead of the transducer functions outweighs the performance benefits.

For these reasons, it is generally a good idea to use transducers sparingly, and only when they provide a significant performance or clarity benefit over other approaches. In many cases, traditional loops or array methods may be a simpler and more performant solution.

Last updated on