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();
[Link]('XBLK-01', 'Extra black car paint');
[Link]('XBLK-02', 'Extra black outdoor paint');
[Link]('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
(").
To insert a variable into your string, bracket it with ${ 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 ([Link]() === [Link]()) {
// 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 ([Link](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 = [Link]('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) {
[Link](`Found ${match[0]} at ${[Link]}`)
}
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 = [...[Link]('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 = [Link](',');
for (var i = 0; i < [Link]; i++) {
animalArray[i] = animalArray[i].trim();
}
But today you can use [Link]() 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 = [Link](',');
animalArray = [Link](s => [Link]());
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 [Link] 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 = [Link]();
// (Do a time consuming task here.)
// Get a DOMHighResTimeStamp object that represents the end time
const endTime = [Link]();
// 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' &&
[Link] > 0) {
// This is a genuine string with text in it
}
7. Weak random numbers
We’ve been using [Link]() 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 [Link](), the Crypto API is actually a pseudo-random number generator. But unlike
[Link](), 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 [Link](). It’s designed to fill a buffer with
random integers, so if you need a fractional value like the kind generated by [Link](),
you need to do a bit of translation. Here’s an all-purpose solution:
const randomBuffer = new Uint32Array(1);
[Link](randomBuffer);
const randomFraction = randomBuffer[0] / (0xffffffff + 1);
Now you can use this random fraction just like you would use the [Link]() 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 = [Link](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.
If you liked this article, check out these modern tricks for JavaScript arrays. And subscribe to
the Young Coder newsletter for a once-a-month email with our best tech stories.