Categories
JavaScript Best Practices

JavaScript Antipatterns — Prototypes, Switch, and More

JavaScript lets us do a lot of things. It’s sometimes too forgiving in its syntax.

In this article, we’ll look at some antipatterns that we should avoid when we write JavaScript prototypes, switch statements, use of eval and parsing integers.

Built-in Prototypes

We should never mess with prototypes of built-in constructors like Array or Function .

If we need extra functionality that they don’t provide, we either make our own class or function.

Messing with built-in prototypes will bring problems with conflicts and people will be confused about why something is added to the prototype or why some methods act differently than from the docs.

Properties that we add to the prototype might also show up in loops that don’t use hasOwnProperty , which is even more confusing.

For instance, we should never write something like:

Array.prototype.foo = function(){
  //...
}

Instead, we write our own function to pass in an array:

function(arr) {
  //...
}

switch Pattern

We can use switch statements to replace a series of if and else statements.

For instance, we can write:

let val = 0,
  result = '';

switch (val) {
  case 0:
    result = "zero";
    break;
  case 1:
    result = "one";
    break;
  default:
    result = "unknown";
}

instead of:

let val = 0,
  result = '';

if (val === 0) {
  result = 'zero';
} else if (val === 1) {
  result = 'one';
} else {
  result = 'unknown';
}

It’s longer but more readable.

We’ve to remember to add break to each case statement so that once a matching case is found, it’ll stop running the other cases.

Also, we should end with a default case so that we do something even if no matching value is found.

Avoiding Implied Typecasting

To avoid implied typecasting, we should avoid using the == operator for equality comparison.

The rules are complex and confusing for type conversions.

Instead, we should use the === operator for comparison:

const foo = 0;
if (foo === false) {
  // not running because foo is 0, not false
}

The type of both operands have to be the same before comparison, so the body won’t run.

Likewise, we use the !== operator for inequality comparison.

So we write:

const foo = 0;
if (foo !== false) {
  // runs because foo is 0, not false
}

If we use the < , > , <= , or >= operators, then we’ve to convert the types of the operators to the same types explicitly to avoid confusion.

Avoiding eval()

eval should be avoided. Security issues arise from running code from strings.

It’s also very hard to debug code that is in strings.

Also, browsers can’t optimize code that is in strings.

Therefore, we should never use it.

setInterval , setTimeout , and Function constructor all take strings.

So we shouldn’t use the Function constructor to define functions.

If we call setInterval or setTimeout , the callback should be a function and not a string.

So instead of writing:

setTimeout(`console.log('hello')`)

We write:

setTimeout(() => console.log('hello'))

Number Conversions with parseInt()

We can call parseInt to get a numeric value from a string.

It takes a 2nd radix parameter to specify the base of the number.

We should include the 2nd argument so we make sure that the number string is converted properly to a number.

For instance, we should write:

const num = parseInt("16", 10);

so that we know that 16 is a decimal number.

However, we should be careful with parseInt since it’ll try to convert strings with numbers and letters.

For instance, parseInt(“1 abc”) returns 1.

We can also use the + operator to convert something to a number by writing:

const num = +"16";

On the other hand, +”1 abc” return NaN , which is probably what we want.

Conclusion

We should never add methods to built-in prototypes.

It just confuses people and it may overwrite existing methods, which is no good.

switch statements may be good alternatives to if and else.

We shouldn’t use eval or related methods like the Function constructor or passing strings to setTimeout or setInterval .

Categories
JavaScript Best Practices

List of JavaScript Antipatterns

JavaScript is a language that’s used a lot in many places including web development and more.

Like any other language, it’s easy to commit antipatterns when we’re programming with JavaScript.

In this article, we’ll look at some antipatterns we should avoid.

Polluting the Global Namespace

Polluting the global namespace is bad because it’ll lead to name collisions,

Without strict mode, it’s also easy to accidentally declare global variables.

For instance, without strict mode, we can write something like:

x = 1;

which is the same as:

window.x = 1;

window is the global object in browser JavaScript.

Global variables are accessible everywhere so that we can change their values anywhere.

This isn’t good because we don’t want to run into bugs because of accidental reassignments.

An example of accidental assignment would be something like:

function foo() {
  return 1;
}

function bar() {
  for (i = 0; i < 10; i++) {}
}

i = foo();
bar();

where we assign i in many places. i was assigned in multiple places in a confusing way.

Extending the Object prototype

We should never add any new properties to Object.prototype .

This is because it’s global. It’ll affect other objects since almost all objects extend the Object.prototype .

If everyone does this, it’s very easy for people to overwrite the Object ‘s prototype with their own code.

Instead, we should use Object.defineProperty to define properties with property descriptors that we want to set if we want control over our object’s properties.

Using Multiple var Declarations Instead of One

Multiple var declarations are less readable and slightly slower, so instead of writing:

var a = 1;
var b = 2;
var c = 3;

we should write:

var a = 1,
  b = 2,
  c = 3;

Using new Array(), new Object(), new String(), and new Number()

There’s no reason to use the Array constructor. It’s confusing since the single argument versions and the multiple arguments version do different things.

The single-argument version takes a number and returns an array with the length set by the argument.

The multiple argument version takes the content of the array as arguments. We can pass in as many as we want to populate our new array.

For instance:

const arr = new Array(5);

returns an array with 5 empty slots.

On the other hand,

const arr = new Array(1, 2, 3);

returns [1, 2, 3] .

Instead, we should use array literals to make things simple for us:

const arr = `[1, 2, 3];`

It’s simpler and less confusing.

Likewise, new Object() is just extra writing. We have to define the properties on different lines as follows if we want to add properties:

const obj = new Object();
obj.a = 1;
obj.b = 2;
obj.c = 3;

Then obj ‘s value is {a: 1, b: 2, c: 3} .

That’s just extra writing. Instead, we should use object literals instead:

const obj = {
  a: 1,
  b: 2,
  c: 3
}

It’s much less typing and also pretty clear.

The problem with new String() and new Number() are that they give us the type 'object' . There’s no reason that we need strings and numbers to have the type 'object' .

We’ve to call valueOf() if we want to convert them back to primitive values.

Instead, we should use the factory functions String() and Number() to convert things to string or number.

For instance, instead of writing:

const str = new String(1);

We write:

const str = String(1);

Also, instead of writing:

const num = new Number('1');

We write:

const num = Number('1');

Relying on the Ordering of Iterator Functions Like .map and .filter

We shouldn’t rely on the iteration order of forEach, map and filter .

The iteration also doesn’t work if we pass in async function as the filter.

If we need to iterate through things in a specific order, then we should sort them the way we want before iteration.

Also, if we want to use the index of the entry, we can pass it into the callback so that we get index reliably.

For instance, we can write the following to get the index and the original array in the callback as follows:

const nums = [1, 2, 3];
nums.forEach((num, index, nums) => {
  if (index === nums.length - 1) {
    console.log('end');
  }
})

This is much better than:

const nums = [1, 2, 3];
let count = 0;
nums.forEach((num) => {
  if (count === nums.length - 1) {
    console.log('end');
  }
  count++
})

Not only the second example more complex, but it’s less reliable since we may accidentally change the count elsewhere.

On the other hand, accessing the index and original array from the callback parameters means there’s no chance of modifying count outside.

This also applies to map and filter .

Conclusion

We can should avoid antipatterns like global variables, unnecessary use of constructors, and extending the Object ‘s prototype.

Also, since we can access the numbers from the parameters, we shouldn’t create variables to track the index on the outside.

Categories
JavaScript Best Practices

How to Write Better JavaScript Modules

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript modules.

Prefer Named Exports

Named exports have to be imported by the name that the member is exported as.

This is different than default exports which can be imported by any name.

Therefore, named exports are less confusing that default exports.

For example, instead of writing:

export default class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}!`;
  }
}

We write:

export class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}!`;
  }
}

The first example can be imported with any name.

The 2nd has to be imported as Person .

Autocomplete also works with named exports if the text editor we’re using has that feature.

No work during import

We shouldn’t do anything with our exported code.

It’s easy to get unexpected results if we export something with an expression that does work.

For example, the following export is no good:

config.js

export const config = {
  data: JSON.parse(str)
};

The work would still be done after the export is done, so we get the latest value.

When we import it, JSON.parse is called.

This means that the import will be slower.

If we have:

import { config } from 'config';

Then the JSON.parse will be run then.

To make JSON.parse run in a lazy fashion, we can write:

config.js

let parsedData = null;

export const config  = {
  get data() {
    if (parsedData === null) {
      parsedData = JSON.parse(str);
    }
    return parsedData;
  }
};

This way, we cache the parsed string in parsedData .

JSON.parse only runs if parsedData is null .

High Cohesion Modules

Cohesion describes the degree to which components inside a module belong together.

We should make sure that a module belongs together.

This means we should have related entities in one module.

For instance, we can make a math module with functions that do arithmetic operations.

We can write:

math.js

export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

All functions do similar things, so they belong in one module.

If we have low cohesion modules, then it’s hard to understand what the module has.

Unrelated things in one module just don’t make sense.

Avoid Long Relative Paths

It’s hard to find a module if they’re nested deeply.

We should avoid deep nesting to avoid long relative paths.

So instead of writing:

import { addDates } from '../../date/add';
import { subtractDates }   from '../../date/subtract';

We write:

import { addDates } from './date/add';
import { subtractDates } from './date/subtract';

If we put them in a Node package, we can use absolute paths:

import { addDates } from 'utils/date/add';
import { subtractDates } from 'utils/date/subtract';

We put everything in a utils package so that we can reference it in an absolute path.

Conclusion

We should make cohesive modules to make understanding them easier.

Also, named exports are better than default ones.

We should export code that does work during export since they’ll run when we import it.

Categories
JavaScript Best Practices

Better JavaScript — State, and Array vs Array-Like Objects

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

No Unnecessary State

APIs can be classified as stateful or stateless.

A stateless API provides functions or methods whose behavior depends on inputs.

They don’t change the state of the program.

If we change the state of a program, then the code is harder to trace.

A part of a program that changes the state is a stateful API.

Stateless APIs are easier to learn and use, more self-documenting, and less error-prone.

They’re also easier to test.

This is because we get some output when we pass in some input.

This doesn’t change because of the external state.

We can create stateless APIs with pure functions.

Pure functions return some output when given some input.

When the inputs are the same in 2 different calls, we get the same output each time.

So instead of writing:

c.font = "14px";
c.textAlign = "center";
c.fillText("hello, world!", 75, 25);

We write:

c.fillText("14px", "center", "hello, world!", 75, 25);

The 2nd case is a function that takes inputs and doesn’t depend on the font and textAlign property as it does in the previous example.

Stateless APIs are also more concise.

Stateful APIs led to a proliferation of additional statements to set the internal state of an object.

Use Structural Typing for Flexible Interfaces

JavaScript objects are flexible, so we can just create object literals to create interfaces with items we want to expose to the public.

For instance, we can write:

const book = {
  getTitle() {
    /* ... */
  },
  getAuthor() {
    /* ... */
  },
  toHTML() {
    /* ... */
  }
}

We have some methods that we want to expose to the public.

We just provide this object as an interface for anything that uses our library.

This is the easiest way to create APIs that the outside world can use.

Distinguish Between Array and Array-Like Objects

JavaScript has arrays and array-like objects.

They aren’t the same.

Arrays have their own methods and can store data in sequence.

They’re also an instance of the Array constructor.

Array-like objects don’t have array methods.

To check if something is an array, we call the Array.isArray method to check.

If it return false , then it’s not an array.

Array-like objects can be iterable or not.

If they’re iterable, then we can convert them to an array with the spread operator.

For instance, we can convert NodeLists and the arguments object to an array:

[...document.querySelectorAll('div')]
[...arguments]

We convert the NodeList and the arguments object to an array.

If it’s a non-iterable array-like object, which is one with non-negative integer keys and a length property, we can use the Array.from method to do the conversion.

For instance, we can write:

const arr = Array.from({
  0: 'foo',
  1: 'bar',
  length: 2
})

Then arr would be:

["foo", "bar"]

Conclusion

We shouldn’t have unnecessary states in our program.

Also, duck typing is good for identifying types.

And we should distinguish between array and array-like objects.

Categories
JavaScript Best Practices

Better JavaScript — Loops and Arrays

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

Don’t Modify an Object During Enumeration

We shouldn’t modify an object during enumeration.

The for-in loop isn’t required to keep current with object modifications.

So we may get items that are outdated in the loop.

This means that we should rely on the for-in loop to behave predictably if we change the object being modified.

So we shouldn’t have code like:

const dict = {
  james: 33,
  bob: 22,
  mary: 41
}

for (const name in dict) {
  delete dict.bob;
}

We have the dict object but we modified it within the for-in loop.

But the loop isn’t required to get the latest changes, so bob might still show up.

Prefer for-of Loops to for-in Loops for Array Iteration

The for-in loop isn’t meant to be used to iterate through arrays.

The order is unpredictable and we get the keys of the item instead of the item itself.

So if we have something like:

const scores = [4, 4, 5, 7, 7, 3, 6, 6];
let total = 0;
for (const score in scores) {
  total += score;
}
const mean = total / scores.length;

score would be the key of the array.

So we wouldn’t be adding up the scores.

Also, the key would be a string, so we would be concatenating the key strings instead of adding.

Instead, we should use the for-of loop to loop through an array.

For instance, we can write:

const scores = [4, 4, 5, 7, 7, 3, 6, 6];
let total = 0;
for (const score of scores) {
  total += score;
}
const mean = total / scores.length;

With the for-of loop, we get the entries of the array or any other iterable object so that we actually get the numbers.

Prefer Iteration Methods to Loops

We should use array methods for manipulating array entries instead of loops whenever possible.

For instance, we can use the map method to map entries to an array.

We can write:

const inputs = ['foo ', ' bar', 'baz ']
const trimmed = inputs.map((s) => {
  return s.trim();
});

We called map with a callback to trim the whitespace from each string.

This is much shorter than using a loop like:

const inputs = ['foo ', ' bar', 'baz ']
const trimmed = [];
for (const s of inputs) {
  trimmed.push(s.trim());
}

We have to write more lines of code to do the trimming and push it to the trimmed array.

There’re many other methods like filter , reduce , reduceRight , some , every , etc. that we can use to simplify our code.

Conclusion

We shouldn’t modify objects during enumeration.

Also, we should prefer iteration methods to loops.

The for-of is better than the for-in loop for iteration.