JavaScript is so flexible, and so quickly evolving, that you can meet a tiny technical requirement in infinite ways.
Even after kicking out broken code and bad-code-that-sort-of-works, lots of remaining approaches can still be equally right and — unless you'll be running them 1000s of times — equally efficient.
So, as with other Code Anatomy posts, the code below isn't the only way to do this.
It's one way that matches what everyone wants from code: clarity, maintainability, extensibility, and scalability (for a short function like this, scalability means delivering performance even with very long inputs).
And it avoids what we don't want: brevity for brevity's sake, extension only via copy-and-pasting more and more lines of code, and so on.
You're welcome to post your alternative utility function to do the same thing. Just make sure it actually works!
Today's task, and how not to do it
One everyday requirement is to check if a list of variables and/or literal values (like a
, b
, c
, "apple"
, and "carrot"
) all contain the the same value.
You'll need this for custom forms logic, in webpage templates, in data management using FlowBoost, all manner of projects.
And new programmers may think there's an easy shortcut. They know that comparing equality of two things[1] is done with:
if ( a === b )
so they try to extend it to:
if ( a === b === c === "apple" )
Nope, not gonna fly. The equality operator doesn't work like that. Because of operator precedence (that is, implicit parentheses) it's equivalent to running:
if ( ( ( a === b ) === c ) === "apple" )
And that's going to be way off what you expect. Consider the outcome when you deliberately set 4 known-equal values (and yes, setting values does work in a “chain” like that!):
var a = b = c = "apple";
Then try to compare them:
if ( a === b === c === "apple" )
As I mentioned above, this is executed like so:
if ( a === b === c === "apple" )
// expands to ⇓
if ( ( ( "apple" === "apple" ) === "apple" ) === "apple" )
// simplifies to ⇓
if ( ( true === "apple" ) === "apple" )
// simplifies to ⇓
if ( false === "apple" ) // nope!
In sum, completely wrong and not what you're trying to do (hopefully, even cursory testing shows this doesn't work and you'll never roll it out, even if you didn't know why until just now).
There is actually an extremely ugly way to get a working one-liner with the ternary (?:
) sequence. We will definitely not be approving this method today, but just for completeness:
if ( a === b ? b === c ? c === d : false : false )
I won't explain why this works (you can play around with it and see). But even before considering its accuracy, consider that it has the same problem as the non-working attempts above: such syntax becomes completely unmanageable if you have more than a few items. With this much extra crud to write (?
and :
and false
for every item) the chance that you will introduce a syntax error or bug approaches 100%.
See, code you write to process a list of things can't break or become unmanageable just because you have more than “several” things. Remember (or learn) the Zero-One-Two-Many principle, which says If there can be three of something, expect 300 of something.
What you want is a method of checking a (practically) limitless set of items, without pasting ===
and ?
and :
and ()
for every new item and guaranteeing a screwup.
Take 1: Deduping a Set or Array
Here's a function that's easy to call, without complicated syntax, for any number of items:
function allValuesEqual(){
var uniqueValues,
arrayFrom;
if (typeof window.Set == "function") {
uniqueValues = new Set(arguments);
} else {
arrayFrom = Array.from || Function.prototype.call.bind(Array.prototype.slice);
uniqueValues =
arrayFrom(arguments)
.reduce(function(prev, next){
if (prev.indexOf(next) == -1) {
prev.push(next);
}
return prev;
},[]);
}
// <= 1 means no args will return true
// for no args to be false, use == 1 instead
return (uniqueValues.size || uniqueValues.length) <= 1;
}
You call this with any number of arguments:
var areTheyEqual = allValuesEqual(a, "apple", b, c, d);
// areTheyEqual is boolean true or false
If you step through the first if
you'll see there's a very cool and maintainable one-liner if your browser supports the JavaScript Set type with constructor arguments. This means Yes to Edge, Chrome, Firefox, and Safari 10, but No to Safari 9.1 nor any version of Internet Explorer.
That one line is:
if (new Set([a, "apple", b, c, d]).size == 1) {
// all values are equal
}
How does this work? The new Set()
constructor call can take an array as an argument, which tells it to add all the elements of that array to the new Set. And a Set object, by definition, automatically strict-dedupes anything you add to it — that is, it will only hold the first copy of any distinct value.
So by adding an array of values to the Set, you guarantee that the Set will be only left with one of every distinct value. If the Set's size
is 1, that means there was only one distinct value in the original array! (Another way of asking “Are all the values equal?” is “How many distinct values are there in this list of N values?”)
Cool, eh?
Now, this method is not necessarily the most efficient, and as we'll see below, there's a better way to do it that doesn't rely on Set support. That better way, sadly, is clunkier to look at, and I sure like the streamlined quality of this Set trick.
The reality in 2018, though, is you have to support at least IE 11 and Safari 9. So allValuesEqual
uses polyfill-type code when typeof window.Set != "function"
. The polyfill basically replicates what new Set(<array>)
does, creating a deduplicated regular Array. Lot less fun to write, though.
The problem with the Set trick
Remember what I said above about the Zero-One-Two-Many rule. Our function to detect if all values are equal must expect lists of 100, 1000, or even 100,000 items. So it needs to behave as efficiently as possible with large list sizes.
The thing about the new Set(<array>)
(and the polyfill for pre-Set browsers) is that they work wonderfully, but they scan every element of the initial array.
On a true
result, that's necessary: a list of 1,000,000 equal items must be scanned all the way through. But a list of 1,000,000 items whose, say, 7th and 8th elements are different does not need to be scanned past the 8th element. Unless you know ahead of time (unlikely) that the vast majority of your outcomes will be true
, it's terrifically inefficient, cool as it is.
Let's do better: Array#every
We can lean on the built-in behavior of the every
method to do this much more efficiently, even if it's less show-offy.
every
runs a function for every element in an array, and it's guaranteed to exit (return early) as soon as it gets a false result. This is the kind of behavior we want: it'll scan a whole array if it needs to, but has the built-in intelligence to scan as few items as possible (we don't have to manually use an old-school break
).
Observe:
function allValuesEqual2(){
var arrayFrom = Array.from || Function.prototype.call.bind(Array.prototype.slice);
// no args will return true
// for no args to return false, use
// return arguments.length && arrayFrom(arguments)
return arrayFrom(arguments)
.every(function(itm, idx, allItms){
return itm === allItms[0];
});
}
Now, that's a nice little routine. The only thing notably inefficient about it is that it dereferences index [0]
(the first item) on every iteration of the array. Deferencing is a pretty cheap operation, but since we're now expecting that there may be a very large array, it would at least look better to eliminate all repetition. (I did run some tests on it and dereferencing can be 33-50% slower with 10K items, depending on browser, even if you wouldn't notice it.[2]) So pull the first item out into a variable first:
function allValuesEqual2(){
var arrayFrom = Array.from || Function.prototype.call.bind(Array.prototype.slice);
// no args will return true
// for no args to return false, use
// return arguments.length && arrayFrom(arguments)
var firstItm = allItms[0];
return arrayFrom(arguments)
.every(function(itm, idx, allItms){
return itm === firstItm;
});
}
allItms[0]
now runs only once, before the iteration. Again, may not make any practical difference, but if a senior dev is looking at your code, it'll save you a *rolleyes* momement.
Also note that I rephrased the initial question before writing this code. Yet another way of asking “Are all the values equal?” is “Are all the values equal to the first value?”)
Being able to flip questions around to create new answers in code, while delivering the same business outcome, is a critical part of “thinking like a programmer.” I would venture that it's more important than coding itself. But it doesn't come overnight or even over a few years, I've gotta warn you. Only recently have I been able to think like this, yet I've been trusted with code for a loooooong time.☺
Notes
[1] Using strict equality ===
here but the same lesson applies to loose equality ==
.
[2] I was going to recommend the this
argument of every
(the optional 2nd argument). But as you can see in my jsPerf, that's still really slow. It kinda looks tighter, though.