Use const, maybe let, not var

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.

For more, see the Airbnb JavaScript Style Guide.

Make strings with templates

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 string created using 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})`
)

Prefer arrow function syntax

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.

Here are two traditional ways to define a function to square numbers:

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

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

The first is a function declaration. It is not an expression with a return value. It declares the definition of square.

The second is an assignment statement, with a function expression on the right-hand side. The value of the expression is a function object that is stored as the value of the variable square.

Function expressions become more useful with higher-order functions like map. The following expression takes a list of numbers and return a list of their squares, without needing to define a square function first.

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

This kind of code happens a lot, so arrow notation was created to make the above code even simpler. Here's one way to do the above with arrow notation:

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

But this can be even simpler. There are two common cases that have a simpler syntax:

  • If there is exactly one simple parameter variable, it can be written without parentheses.
  • If the body is just a return expression, the body can be written without the braces and return.

So the list of squares expression can be written as just

numbers.map(x => x * x)

There is one important difference between functions created with arrow expressions and functions created with function expressions. When a function created with a function expression is called, this is bound to the "execution context". That does not happen for arrow functions. This difference only matters if you use JavaScript classes or object methods.

Use array iterative methods

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

Make a list from a list

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 a sublist

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 something in a list

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 getOffcice = (role, members) => (
  members.find(member => member.role === role)
);

To collect a value

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.

A very common mistake with reduce() is to forget to given an initial value as the second argument. It's easy to forget because it's usually something like zero, [], or {}, and comes after a long function form. If it's not given, the first element of the list is used. Sometimes that works but often you'll get an error or incorrect answer. Always give the initial value.

Use async and await, not promise/then

Asynchronous code is tricky. The goal is to wait for some data without stopping the normal execution of a program. For example, while getting data from a server, or doing a complex calculation, you do not want a web app to freeze the browser. Instead of returning a value immediately, we want to specify code to run with a value, whenever it becomes available.

Promises

A common solution in modern languages is to use promises. A promise object holds code to be executed when some data is returned by some other code. A promise is said to be resolved when it has the data it was waiting for. A JavaScript promise object has a method then(function). This can be used to tell the promise to call function with the response object when the promise is resolved.

The then(function) method returns a new promise. If function returns a value, you could add a second then(function2) to specify the function to call with the value returned by the first function.

The JavaScript function fetch(url) fetches data from a URL. The data is contained in a response object. Rather than wait, fetch() returns a promise object. Therefore, we could define a function to display the response object, when it arrives, like this:

const showServerData = () => {
  fetch('https://some-server.com/data/x/y')
  .then(response => console.log(response))
}

If the response contains JSON, you can get it by calling response.json(). Because extracting JSON may take time, this method actually returns another promise, so to get and display the JSON returned from a server, we could write this:

const showServerData = () => {
  fetch('https://some-server.com/data/x/y')
  .then(response => response.json())
  .then(json => console.log(json))
}

await and async

In modern JavaScript, you can define showServerData() a bit more readably:

const showServerData = async () => {
  const response = await fetch('https://some-server.com/data/x/y')
  console.log(await response.json()
}

This says "wait for the response then wait for the JSON." Though it looks very different, this function executes just like the previous version. Promise objects are created with code to call when the promise is resolved, every time JavaScript sees the await operator.

await can only be used inside functions that are marked with the async keyword.

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://some-server.com/data/x/y')
  const json = await response.json()
  return json.name
}

console.log(getName())

This will not print a name, It will print a promise object. To get access to the promise value, when it resolves, you must use an await or then(), e.g.,

const getName = async () => {
  const response = await fetch('https://some-server.com/data/x/y')
  const json = await response.json()
  return json.name
}

getName().then(name => console.log(name))

Use numeric timestamps, not date/time strings

Store dates as numeric timestamps

Only use strings to display dates to users or communicate with servers that require strings, such as Github. In your code, use Unix Time Stamps. This is a large integer that is easy to compare and store.

Convert a date string to a time stamp

No need to create a Date object first, e.g.,

const ts = new Date('September 28, 2018 08:00:00').getTime();

Use the static Date parse() method:

const ts = Date.parse('September 28, 2018 08:00:00');

Convert a time stamp to a date object

const date = new Date(1538053200000);

Only format numbers when displaying

Round numbers, but only when displaying

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>`

When displaying, use Number's toFixedPoint() method.

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

Format currency in local style

Don't roll your own currency formating code.

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

You are almost certain to get something wrong. Use JavaScript's internationalized formating features.

`<span>You owe ${new Intl.NumberFormat('en-US', {style: 'currency', currency:'USD'}).format(amount)}>span>`"

© 2021 Chris Riesbeck
Template design by Andreas Viklund