Do This Not That, JavaScript Edition
By: Matthew Macdonald
Tags: Refactoring, JavaScript, Programming, Ecmascript, ES6

Do This Not That, JavaScript Edition

Keep these obsolete patterns out of your code

Courtesy of Icons8

Recently, I wrote about my new labor of love — replacing old JavaScript code with more modern practices for the tech publisher O’Reilly. The job involves staring down some dust-collecting scripts from five or ten years ago. In a short amount of time, I’ve seen plenty of old routines that still work, but don’t reflect today’s best approaches.

Which raises a good question. When is it time to intervene? If you’re dealing with proof-of-concept examples and company standards, you want the best possible solution every time. But if you’re looking a complex and established codebase, there’s a good case to be made for stepping carefully. Make changes when you need to touch the code for another reason (for example, to add an enhancement). Make changes when you’re opening up an old module to add test coverage. Make changes before you consider using the same code or approach in a new project. And — word to the wise — make changes when you have a chunk of free time.

How old code pollutes new projects

There’s plenty to learn from refactoring old JavaScript code. It’s a free lesson if you want to firm up your understanding of the language. Which got me thinking that I should share some of the old practices I’ve encountered, and a few the changes I’ve made. Here are 7 notable cases that I’ve seen so far.

Note: I’ve tried to keep the old code examples as vintage as possible. Expect to see a few var keywords.

1. Associative arrays

These abominations have survived for a surprisingly long time. The basic idea is to hijack the way JavaScript lets you add properties to any object, and use it to make fake array “items” indexed by a key name. One approach is to do it directly to an Array object:

var products = new Array(); // It looks like I'm adding items to this array,

// but I'm actually adding properties!

products['XBLK-01'] = 'Extra black car paint';

products['XBLK-02'] = 'Extra black outdoor paint';

products['CLK-01'] = 'Generic brand caulking';

This is terrible, because it looks like an array, but none of the “items” show up if you iterate over the array, and none of them are counted in the length property.

A barely better approach is to the same thing, but create a custom Object instead of an Array . But since ES6, neither workaround is useful, because this string-indexing pattern has a formal solution. It’s called the Map object:

const products = new Map();

products.set('XBLK-01', 'Extra black car paint');

products.set('XBLK-02', 'Extra black outdoor paint');

products.set('CLK-01', 'Generic brand caulking');

Note that the Map object expects you to add items with the set() method. (It’s technically possible to use the associative-array approach to add new properties to a Map object, but that’s just wrong in every way.)

Learn more: Here’s Mozilla’s Map reference.

2. Old-fashioned concatenation

If you’ve worked with JavaScript for a few years, you’ve probably written plenty of string concatentation code. The basic approach is to join everything together with the + operator:

var employeeDetail = 'Our team includes ' + firstName + ' ' +

lastName + ' who works on the ' + team + 'team. They/'ve been a' +

' team member since ' + hireDate + '!';

This works perfectly well, although it’s notoriously easy to drop a space before or after the variable, or (worse) miss an apostrophe next to a string literal.

ES6 introduced template literals, a minor feature that can significantly improve the readability of this kind of code:

const employeeDetail = `Our team includes ${firstName} ${lastName} who works on the ${team} team. They've been a team member since ${hireDate}!`;

To use this approach, you just need to remember four details:

You delimit string literals with the backtick character ( ` ), not an apostrophe ( ' ) or quote ( " ).

), not an apostrophe ( ) or quote ( ). To insert a variable into your string, bracket it with ${ at the start and } at the end.

at the start and at the end. Hard returns are preserved in template literals, so don’t hit that Enter key unless you really want a line break in your string.

You can use expressions and call functions in a template literal. But as a rule of thumb, only do so when the meaning is transparent and you can still easily read the string. So calling formatDate() is reasonable, but putting a calculation involving multiple variables and functions is not.

Learn more: Here’s a comprehensive review of template literals.

3. Comparing case without considering locales

How do you perform a case-insensitive comparison of two strings? It seems like the easiest possible question — you just put both strings in the same case:

var a = "hello";

var b = "HELLO"; if (a.ToLowerCase() === b.ToLowerCase()) {

// We end up here, because the lowercase versions of both

// strings match.

}

But this technique has some edge cases with accents and different languages. (Turkish is a notable example.) To make it work safely everywhere in a professional-level application, you need a more robust locale-specific comparison:

const a = "hello";

const b = "HELLO"; if (a.localeCompare(b, undefined, {sensitivity: 'accent'}) === 0) {

// We end up here, because the case-insensitive strings match.

}

Leave the second parameter undefined to use the current computer’s locale. But note the sensitivity property. Set it to accent (as shown here) and characters that have different accents (like a and á) are treated as unequal. Set it to base and you’ll get a more permissive case-insensitive comparison that treats all accented letters as matches.

Learn more: Here’s a reference that introduces internationalization and locale settings

4. Iterating over strings

Old school JavaScript loves the string manipulation methods indexOf() and slice() (or, even older, search() and substring() ). And for many string parsing tasks, these methods are still important. But over the years, JavaScript has added cleaner approaches using functional programming paradigms. If they work for your string-handling task, it usually leads to simpler code.

For example, let’s say you want to search a string for a specific word or pattern. (In non-trivial applications, you’ll probably be using regular expressions, but let’s assume for the moment you’re looking for a fixed string.) In the old days, you’d use indexOf() to find a character position, and then keep advancing from that position to find more matches. Today, matchAll() gets an iterator that you can slip right into a foreach loop:

const searchText = 'I know not where I was born, save that the castle was infinitely old and infinitely horrible'; // Match a word (or replace this string with a regex for more power)

const matches = searchText.matchAll('infinitely'); // Iterate directly on the text, searching as you go.

// This gives you the best possible performance, with no

// character counting or arrays needed.

for (const match of matches) {

console.log(`Found ${match[0]} at ${match.index}`)

}

Or, if you’re holding the results for later, combining matchAll() with the spread operator puts all the results into an array in one step:

const matches = [...searchString.matchAll('infinitely')];

Here’s another common example. Let’s say you want to split a list into an array of words and trim the results. In the past, that required procedural code like this:

var animalList = 'horses, monkeys, goats, pandas, iguanas';

var animalArray = animalList.split(','); for (var i = 0; i < animalArray.length; i++) {

animalArray[i] = animalArray[i].trim();

}

But today you can use Array.map() to execute a function on every item in the array and replace that item, all in one tidy step:

const animalList = 'horses, monkeys, goats, pandas, iguanas';

const animalArray = animalList.split(','); animalArray = animalArray.map(s => s.trim());

Learn more: Brush up on the matchAll() method, the map() method, and the spread operator.

5. Timing performance with Date

We all know you can get the current time by creating a new Date object with the parameterless constructor, or using the static Date.now property. Subtract one Date from another, and you’ve got the milliseconds in between. Easy!

But Date isn’t the best approach for reliable profiling — for example, when you want to time the execution of a script or choose between different algorithms for a task. The right tool for this job is the Performance API:

// Get a DOMHighResTimeStamp object that represents the start time

const startTime = window.performance.now(); // (Do a time consuming task here.) // Get a DOMHighResTimeStamp object that represents the end time

const endTime = window.performance.now(); // Find the elapsed time in milliseconds

const elapsedMilliseconds = endTime - startTime;

Unlike Date -based calculations, the Performance API is the most accurate fractional millisecond count that’s supported on the current hardware.

Learn more: Here’s a jumping-off point for the many layers of the Performance API. There’s lots there for more advanced profiling.

6. Testing strings for truthy values

JavaScript is notoriously freewheeling with data types, and one function can’t necessarily trust that it’s getting the right data type from another one. Here’s an old-fashioned shortcut that string-processing code sometimes uses:

if (unknownVariable) {

/* We get here as long as:

unknownVariable has been declared

unknownVariable is not null

unknownVariable is not the empty string ''

*/

}

This works because null values, undefined values, and empty strings are all falsy in JavaScript. But this approach has a blindspot. If you pass in the number 0, that also evaluates to false, skipping the if block.

A better bet is to test your inputs and explicitly convert your value to a string first, before you launch into any string-manipulating code. (Use toString() please, not the blank-string concatenation trick.)

Bonus: Get your strict on with this test, which ensures you have a legitimate, non-empty string. And because JavaScript won’t evaluate the second condition unless the first is true, this code is error-free for any type of data, even null or undeclared :

if (typeof unknownVariable === 'string' &&

unknownVariable.length > 0) {

// This is a genuine string with text in it

}

7. Weak random numbers

We’ve been using Math.random() since time immemorial. Which is fine, if you can accept random numbers that can be guessed and reverse-engineered. But if you need something stronger, there’s the Crypto API.

Like Math.random() , the Crypto API is actually a pseudo-random number generator. But unlike Math.random() , the Crypto API uses higher entropy sources of randomness to seed the pseudo-random number generator. It’s up to the operating system to decide how the seed is generated, but it typically involves a combination of recently recorded hardware values like keyboard timings, mouse movements, and hardware readouts.

The Crypto API isn’t quite as user-friendly as Math.random() . It’s designed to fill a buffer with random integers, so if you need a fractional value like the kind generated by Math.random() , you need to do a bit of translation. Here’s an all-purpose solution:

const randomBuffer = new Uint32Array(1);

window.crypto.getRandomValues(randomBuffer);

const randomFraction = randomBuffer[0] / (0xffffffff + 1);

Now you can use this random fraction just like you would use the Math.random() value. For example, if you want a random integer between some min and max value, you use the standard formula:

// Get an integer from min to max.

randomInt = Math.floor(randomFraction*(max-min+1)) + min;

There’s one big disclaimer with this approach — it’s only as secure as the location of your code. For JavaScript that runs in the browser, it’s always possible for a malicious user to tamper with your logic. But for JavaScript that runs on the server (say, in Node), better random numbers have real value.

Learn more: Here’s a comprehensive reference for the Crypto API.