Use const

Originally JavaScript had one keyword to introduce a new variable: var. Most JavaScript code you see online uses it. But modern style recommends const for most code, let otherwise, and var never.

Here are two simple examples of common uses of const:

const table = { a: 1, b: 2 };

const lookup = key => table[key] || 0;

const declares a variable with a value. An error will occur if code tries to re-assign the variable. Therefore, when a developer sees a const, they know what the variable holds for the rest of the code. This makes code maintenance much simpler. It also catches accidental assignments, e.g., when a variable name is mistyped.

By restricting re-assignment, const encourages a functional rather than imperative programming style. This also makes maintenance easier.

Rarely, you may need a variable that can be reassigned, for example, in a classic for loop. In that case, use let.

for (let i = 0; i < n; ++i) {
...
}

let variables have narrower scope. The scope of the loop variable i in the code above is just the for loop. If you used var instead, the scope of i would be the entire function in which the for appears.

But you rarely need a for loop. Use array iteration methods instead.

For more, see the Airbnb JavaScript Style Guide.

Use template strings

A common task in JavaScript is creating a string from data. This used to be done using string concatenation. For example, to define the function describeLine(2, 4, 8, 11) to return the string "The line goes from (2, 4) to (8, 11)", we'd write something like this:

const describeLine = (x1, y1, x2, y2) => (
  'The line goes from (' + x1 + ', ' + y1 + ')'
  + ' to (' + x2 + ', ' + y2 + ')'
);

This line is hard to type, read, and edit.

Modern JavaScript provides template literals. These are strings delimited by backquotes that let you embed data directly. The above becomes

const describeLine = (x1, y1, x2, y2) => (
`The line goes from (${x1}, ${y1}) to (${x2}, ${y2})`
);

Use arrow functions

Functions have become more and more central to JavaScript programming. For this reason, an alternative more compact and semantically simpler way of defining functions was added to JavaScript. For example, the following expression uses the array method map() to take a list of numbers and return a list of their squares.

numbers.map(function(x) { return x * x; })

With arrow notation you can write:

numbers.map((x) => { return x * x; })

But this can be even simpler. Since the body of the function just returns an expression value, you can write the expression directly:

numbers.map(x => x * x)

Arrow notation can also be used to defined named functions. Instead of

function square(x) { return x * x; }

you can write

const square = function (x) { return x * x; };

There is one important difference between functions created with arrow expressions and functions created with function expressions. Calling a function created with a function expression binds the variable this to the "execution context". That does not happen for arrow functions. How this works is JavaScript is complicated. You shouldn't need it.

Use array methods to iterate

You should rarely need to write a for, forEach, or for ... of loop. The array object provides much better solutions

Make lists with map

Suppose you have a list members of data about members of a club, where a single member object looks like this:

{ name: { first: 'John', last: 'Smith' }, email: 'jsmith@example.com', ... }

We can get a simple list of member names, in the form "Smith, John", in one line, using the array map() method:

members.map(member => `${member.last}, ${member.first}`);

Most uses of map() look like this. Occasionally, you want to map over two lists in parallel. To do this, you can take advantage of the fact that map() actually calls the function with two arguments for each element of a list: the element and its zero-based position in the list. We can use that position as an index to get the corresponding element in another list.

For example, to define makePairs(lst1, lst2) to take two lists and return pairs of corresponding elements:

const makePairs = (lst1, lst2) => (
  lst1.map((x1, i) => [ x1, lst2[i] ])
);

Make sublists with filter

Suppose we want to define activeMembers(members) to take a list of members and returns the ones who are active.

The array method filter() is designed for this. Like map(), it takes a function and calls that function with each element of the list. filter() returns a list of all elements of the original array for which the function returned a "truthy" value, i.e., not false, null, zero, "", undefined, or NaN.

So our activeMembers(members) is just

const activeMembers = members => (
  members.filter(member => member.isActive)
);

This is far simpler than the corresponding for loop version.

Find things with find

Suppose our club member objects also includes a role for the officers of the club, e.g.,

{ name: { first: 'Mary', last: 'Brown' }, role: 'president', ... }

The array find() method takes a function. It returns the first element in the array that makes the function return a truthy value. So, to define getOfficer() to take a role and a list of members, and return the member with that role, if any, we write

const getOfficer = (role, members) => (
  members.find(member => member.role === role)
);

Collect values with reduce

Suppose we want to count how many active members there are. We could write

members.filter(member => member.isActive).length

This works, but constructs a throwaway list that the JavaScript interpreter will have to garbage collect. A more efficient solution is to count without creating any new list.

Counting, summing, and so on, are aggregation algorithms. You initialize a result variable to some value, e.g., zero or the empty list. Then you take each element in a list and update the result appropriately. When there are no more elements, you return the result value.

The array method reduce() implements this aggregation algorithm. All you need to give it is the initial value and a function to update the result. The function given is passed four arguments: the current result, an element of the list, the index of that element, and the list. Normally you just specify and use the first two arguments.

The key to using reduce() is the update function. It should always return a value. It should never return undefined. For example, here's how to count the active members with reduce():

members.reduce((count, member) => member.isActive ? count + 1 : count, 0)

The following code would not work

members.reduce((count, member) => { if (member.isActive) return count + 1; })

This code returns undefined for a non-active member. That makes count undefined from that point on.

Always give reduce() an initial value in the second argument. It's easy to forget because it's usually something short, like zero, [], or {}, and appears after a long function form. If no initial value is given, reduce() will use the first element of the list. That will often be wrong.

Use async and await

Asynchronous code is increasingly common in modern JavaScript programming. For example, many applications fetch data from a file or the web. The function fetch() that gets data from a server is asynchronous. It can't get an answer immediately, so there needs to be some way to call it and say "when an answer comes back, run this other code".

In older JavaScript, this was done with callback functions. For example, a function to fetch data from a URL might be called like this:

getData(url, (data) => { displayData(data); })

Then Promise objects were developed. Now functions like fetch() are defined to return a Promise object. A Promise object has a then() method that takes the callback function to call when some data became available so fetch() could be called like this:

fetch(url).then((response) => { ...do something with the response...})

The JavaScript language itself was extended to make using promises simpler. Two new keywords were added: async to mark functions that have asynchronous code, and await to get the value from an expression that returns a promise. await can only be used inside functions that are marked with the async keyword.

A very common example is fetching JSON data. This involves two asynchronous steps: an asynchronous call to fetch() to get a Response object, and another asynchronous call to the Response method json() to get the actual JSON data. Here's an example function that fetches and prints a random quote in the browser console:

const showQuote = async () => {
  const response = await fetch('https://dummyjson.com/quotes/random');
  const json = await response.json();
  console.log(`${json.quote} -- ${json.author}`);
};

This says "wait for the response then wait for the JSON." showQuote() is an asynchronous function. It returns a Promise. In this case, when the promise is resolved, the value is undefined because showQuote() doesn't return anything.

A common mistake is to think that async functions return values like normal functions. Here's an example of bad code

const getName = async () => {
  const response = await fetch('https://dummyjson.com/users/1');
  const json = await response.json()
  return json.firstName;
};

console.log(getName())

This will not print a name, It will print a promise object.

Use timestamps

Store dates as numeric timestamps not date strings

In your code, save dates using JavaScript timestamps. These are simple integers easily converted back to a date object or string as needed.

A timestamp integer is unambiguous, easy to store and read, and easy to compare. A JavaScript timestamps is the number of milliseconds since January 1, 1970 UTC. Date.now() returns the same answer no matter what time zone you are in.

To get the current date and time as a timestamp,

Date.now()

To convert a Date object to a storable timestamp, use the Date method getTime().

const jsReleaseDate = new Date('04 Dec 1995 00:12:00 GMT');
jsReleaseDate.getTime()

To display a stored timestamp, construct a Date from the timestamp, and call the Date method toLocaleString()

new Date(jsReleaseDate).toLocaleString()

For more control over date formatting, use Intl.DateTimeFormat.

Don't round numbers

Use formatting to display round numbers

Don't round numbers in internal calculations. That makes calcuations less accurate. But don't display all the decimal places either. That looks silly.

`<span>Your grade average is ${gpa}>/span>`

For simple floating point numbers, use Number's toFixedPoint() method.

`<span>Your grade average is ${gpa.toFixedPoint(2)}>/span>`"

But don't do this for money!

`<span>You owe $${amount.toFixed(2)}>span>`"

Use JavaScript's internationalized formating features.

const formatMoney = new Intl.NumberFormat('en-US', {style: 'currency', currency:'USD'}).format;
`You owe ${formatMoney(12345.6)}`

© 2024 Chris Riesbeck
Template design by Andreas Viklund