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.

Categories
JavaScript Best Practices

Better JavaScript — Event Queue

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 Block Event Queue on I/O

JavaScript is a single-threaded language, so blocking I/O with synchronous code is bad if the synchronous code is long-running.

One piece of code can hold up the whole program if we block the event queue.

So we shouldn’t have code like:

const text = downloadSync("http://example.com/file.txt");  
console.log(text);

in our program.

downloadSync waits for the text to be downloaded and then it returns it.

This can take a long time.

Therefore, JavaScript provides us with ways to run code asynchronously.

Instead, we can write:

downloadAsync("http://example.com/file.txt", (text) => {  
  console.log(text);  
});

If we run async JavaScript code, then code is suspended until the result is obtained.

We know the result is obtained when the callback is called.

When the code is suspended, then something queued after this piece of code can run.

We don’t have to worry about some object or variable changing from under us because of concurrently executing code if we structure our code correctly.

Use Callbacks for Asynchronous Code

If we have some simple async code, then we can use callbacks to run our code.

For instance, we had:

downloadAsync("http://example.com/file.txt", (text) => {  
  console.log(text);  
});

that takes a callback that’s called when the result is retrieved from the server.

This is the simplest way to create async code.

When downloadAsync runs, then the function is initiated but hasn’t performed the operation yet.

Once the operation is performed, the callback is run and we get the text .

If we have to run more than one async operation in sequence, then we need to run the 2nd one inside the callback of the first one.

So we can write:

downloadAsync("http://example.com/foo.txt", (text1) => {  
  console.log(text1);  
  downloadAsync("http://example.com/bar.txt", (text2) => {  
    console.log(text2);  
  });  
});

We nest the 2nd downloadAsync call inside the first one.

If there’re more async callbacks we need to run in sequence, then we get more nesting.

This is definitely a problem since we don’t want to nest them.

To avoid this, we can use promises.

Use Promises for Complex Async Operations

If we have lots of async code we need to run in sequence, then the nested async callbacks would be too confusing.

We don’t want to have many levels of nesting in our code since it’s hard to debug and trace.

Instead, we use promises to avoid all the nesting.

For instance, we can create a promise chain by writing:

promise  
  .then((val) => {  
    return promise2;  
  })  
  .then((val) => {  
    return promise3;  
  })  
  .then((val) => {  
    return promise4;  
  })  
  .then((val) => {  
    return promise5;  
  })

We call then with a callback that returns a promise to call another promise.

val has the value that the promise resolved to.

Promises can also be rejected.

To catch errors from rejected promises, we call the catch method:

promise  
  .catch((err) => {  
    console.log(err);  
  })

This is much better than nesting callbacks.

Conclusion

We shouldn’t block I/O of a JavaScript code with long-running synchronous code.

If we have anything that’s potentially long-running, we should use async callbacks or promises.

Categories
JavaScript Best Practices

Better JavaScript — Coercion and Chaining

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 Excessive Coercion

There should be no excessive data type coercion in our app.

JavaScript is very loose with types, so it coerces data types all the time.

Coercion may be convenient, but it’s easy to produce unexpected results since the rules are hard to remember.

Coercion is confusing when we work with overloaded function signatures.

So if we have:

function foo(x) {
  x = Number(x);
  if (typeof x === "number") {
    //...
  } else {
    //...
  }
};

We converted x to a number before running the rest of the code, so the type of x is always a number.

The else block will never be executed before of the coercion.

If we overload a function by checking the type of the parameter, then we shouldn’t coerce the data type.

Otherwise, we remove the if-else blocks since we don’t need them.

Instead, we can write:

function foo(x) {
  if (typeof x === "number") {
    //...
  } else if (typeof x === "object" && x !== null) {
    //...
  }
};

to check the data type of x for the types before running the code inside the blocks.

We can add type guards with a function.

For instance, we can write:

const guard = {
  guard(x) {
    if (!this.test(x)) {
      throw new TypeError("unexpected type");
    }
  }
};

We check the type with some test method.

When that returns false , we throw an error.

Other objects can use the guard object as its prototype and implement the test method themselves to do the type check.

To create an object with the given prototype, we can call Object.create on it:

const obj = Object.create(guard);

Support Method Chaining

Method chaining makes doing multiple operations in one statement easy.

For instance, with a string, we can call replace multiple times:

str.replace(/&/g, "&")
   .replace(/</g, "&lt;")
   .replace(/>/g, "&gt;")

This is because replace returns a string with the replacements done.

Eliminating temporary variables make it easier to read the code because there’s less distraction.

We can do this with our own methods by returning this .

For instance, we can write:

class Element {
  setBackgroundColor(color) {
    //...
    return this;
  }

  setColor(color) {
    //...
    return this;
  }

  setFontWeight(fontweight) {
    //...
    return this;
  }
}

to create a class with methods that return an instance of itself.

This way, we can chain each method and get the latest result.

For instance, we can use it by writing:

const element = new Element();
element.setBackgroundColor("blue")
  .setColor("green")
  .setFontWeight("bold");

This makes our lives easier since we don’t need any intermediate variables to get to the final result.

Other popular methods that let us change methods include some array methods and jQuery methods.

Conclusion

We shouldn’t coerce data types when we don’t need to.

Also, making methods chainable is also easier for everyone.

Categories
JavaScript Best Practices

Better JavaScript — Async Code

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.

Error Handling with Async Code

Like synchronous code, async code errors also need to be handled.

Handling async code errors may be trickier than synchronous code.

Synchronous code errors can be caught with a try-catch block:

try {
  f();
  g();
  h();
} catch (e) {
  //...
}

We catch the errors in the catch block.

And e has whatever is throw in the code in the try block.

Async code comes in a few forms.

If it’s a callback, then some may send the error with the callback.

For instance, Node style callbacks send the error:

fs.readFile('/foo.txt', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

In the fs.readFile method, err has the error that’s set when there is one.

We can check for the err value and do something.

Promise errors come in the form of rejected promises.

A promise can be rejected with an error object.

For instance, we can write:

Promise.reject(new Error('error'))

Then we can call the catch method to catch it:

Promise.reject(new Error('error'))
  .catch(err => console.error(err));

The callback’s err parameter has the error.

The async and await looks more like synchronous code.

We can use try-catch to catch rejected promises.

For instance, we can write:

async function foo() {
  try {
    const val1 = await f;
    const val2 = await g;
    const val3 = await h;
  } catch (err) {
    console.error(err);
  }
}

We just wrap our code with try and catch errors with catch like synchronous code.

Async Loops

Async code can be run sequentially in a loop.

The for-await-of loop lets us run async code sequentially.

For instance, we can write:

async function foo() {
  for await (const p of promises) {
    const val = await p;
    console.log(val);
  }
}

We have the for-await-of loop inside the async function which runs the promises in sequence.

It can work with any async iterable object.

Async Callback and Recursion

Async callbacks don’t have any way to run them sequentially easily.

The only way we can do it is with recursion.

For instance, we can write:

function downloadOneAsync(urls, onsuccess, onfailure) {
  const n = urls.length;

  function download(i) {
    if (i >= n) {
      onfailure("error");
      return;
    }
    downloadAsync(urls[i], onsuccess, () => {
      download(i + 1);
    });
  }
  download(0);
}

We have the download function that runs recursively.

The success callback runs the download function once the previous call is successful.

It runs until i reaches n and then stops.

Don’t Block Event Queue on Computations

We shouldn’t block the event queue on complex computations.

If we have some really long-running, then we can create a worker to run it.

Then the task will run in the background.

For instance, we can create a Worker instance by writing:

const myWorker = new Worker('worker.js');

Then we can send messages to it by writing:

myWorker.postMessage(value);

We can listen to messages from the worker by writing:

myWorker.onmessage = function(e) {
  const result = e.data;
  //...
}

Then in worker.js , which is the worker, we can get the message by writing:

onconnect = function(e) {
  const port = e.ports[0];

  port.onmessage = function(e) {
    const data = e.data;
    //...
    port.postMessage(workerResult);
  }
}

The port gets the connection to the main thread.

And e.data gets the data from the main thread sents with myWorker.postMessage .

We can send messages with the postMessage method.

Conclusion

We can loop through promises sequentially.

Also, we can create web workers to run long-running tasks in the background.

The only way to run a series of async callbacks sequentially is with recursion.

Categories
JavaScript Best Practices

Better JavaScript — Arrays, Undefined and Conventions

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.

Use Array Methods on Array Like Iterable Objects

Array methods can be used on array-like iterable objects by using the spread operator.

For instance, we can write:

const divs = [...document.querySelector('div')];

Then we return an array of DOM elements.

querySelector with a NodeList of DOM elements.

Then we used the spread operator to turn the array of elements into an array.

We can do the same thing with strings and other iterable objects.

If we have a non-iterable array object, then we can convert it to an array with the Array.from method.

For instance, we can write:

const arrayLike = {
  0: "a",
  1: "b",
  2: "c",
  length: 3
};

const arr = Array.from(arrayLike);

We have numeric indexes and the length property in the object so we can use Array.from to convert it to an array.

Prefer Array Literals to the Array Constructors

Array literals are shorter than using the Array constructor to create arrays.

So we should use the array literals to create our arrays.

We can write:

const a = [1, 2, 3, 4, 5];

instead of:

const a = new Array(1, 2, 3, 4, 5);

There’re problems with the Array constructor.

If we have to make sure Array hasn’t been modified.

And if it has one argument, it works differently than if it has multiple arguments.

If it has one argument, then it creates an empty array with the given number of slots if the argument is a number.

If it has multiple arguments, then it creates an array with the arguments.

Maintain Consistent Conventions

When we create libraries, we should create interfaces with consistent conventions so we don’t have to look up the names all the time.

For instance, we create constructors with Pascal case names.

And we create functions, variables, and properties with camelCase names.

For function, if we have arguments, then they should have consistent types and order.

This way, we can predict the order and type and work faster.

Sticking to conventions reduces confusion and makes working with them more predictable since we don’t have to look up stuff all the time.

Treat undefined as No Value

undefined is a speicla value in JavaScript.

It means no specific value.

Unassigned values has value undefined .

So if we have:

let x;

then x is undefined .

If we have a return statement without a value, then it returns undefined .

So if we have:

function f() {
  return;
}

Then f() returns undefined .

This is the same as:

function g() { }

And g() also returns undefined .

Function parameters that have bo argument passed into it is undefined .

For instance, if we have:

function f(x) {
  return x;
}

Then calling f without an argument would make x undefined .

Treating undefined as the absence of a value is convention established by the language.

Conclusion

Array-like objects can be converted to an array with the spread operator.

Also, APIs should have consistent interfaces and conventions.

And undefined should be treated as no value.