DEV Community

loading...

A neat little trick with JavaScript's indexOf()

Patrick Werner
YO
・1 min read

Here's a little trick I picked up while browsing through some plugins I had to edit or extend again

var arr = [ 1, 2, 3, 'foo' ];

// old way
if (arr.indexOf('foo') > -1) {
    console.log('"foo" is in "arr"!');
}


// new way
if (~arr.indexOf('foo')) {
    console.log('"foo" is in "arr"!');
}

Enter fullscreen mode Exit fullscreen mode

I wondered - how does that work? It's because of this little operator I never used or seen yet:

The Bitwise NOT-Operator ~

It flips all bits of a number, I'm not sure how it exactly works but it seems that -1 is the only case of ending up with a falsy number, or rather a falsy expression: 0

So that means, we can take advantage of it and use it in indexOf (haven't seen any other uses yet though).

// you can also negate the statement, no separate parantheses needed
if (!~dailyRoutine.indexOf('β˜•οΈ')) {
    console.log('Not a life worth living');
}
Enter fullscreen mode Exit fullscreen mode

What do you think about this? I think it sure looks neat and saves up some characters in the code, but it could confuse people who read it afterwards (or look it up like me lol)

Discussion (17)

Collapse
ijlee2 profile image
Isaac Lee

Unless one will reuse the index in other places, I like to recommend includes instead of indexOf. I think it's more readable.


if (arr.includes('foo')) { ... }

Collapse
thehanna profile image
Brian Hanna

Lack of IE support is a huge bummer since I still have to support IE11 for all my work projects, but that's definitely more readable

Collapse
ijlee2 profile image
Isaac Lee

Indeed, Brian. I have to support IE11 too. I believe the polyfill solution that others have mentioned here would help.

Cheers,

Collapse
alainvanhout profile image
Alain Van Hout • Edited

It's a neat trick, but 'saving a few characters' is a notoriously bad reason to do something a certain way (unless saving a few characters is the prime motivator in your specific use-case). That's because there is a big difference between 'concise' and 'dense'. The former is somewhat preferable, all things being equal. The latter is to be avoided at all cost (again, unless your specific use-case demands it).

Beyond that, to a degree this is similar to using the following approach to declare a default value:

const foo = bar || 'my default value';

That serves as a clean way to do that if it's a convention that your team has decided upon.

Collapse
werninator profile image
Patrick Werner Author

I mean, if you strike it as bad, it's your convention or set of rules. I can see where you're coming from though and probably many people think so too. Both the bitwise NOT operator for indexOf and your code snippet are based on relying on the truthfulness of an expression.

But I think it's one of JavaScript's most interesting feats that it can work like that. That's the reason I wrote this, because I think it's interesting that this works at all.

Collapse
alainvanhout profile image
Alain Van Hout

It's indeed interesting, no doubt about that :).

It reminds me a bit of the following line of JavaScript, that was making the round a couple of years ago, and which manages to showcase a lot of neat JS with very little code: (source)

[].forEach.call($$("*"),function(a){
  a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)
})

Back to the original topic, I wrote my response from the perspective of 'should this be used?', which isn't so much a matter of interestingness as it is one of maintainability. That's not the same as saying it's 'bad', but in any setting where you're not the only contributing developer, code generally has to do better than simply not being bad.

(of course I'm ignoring the already mentioned String.prototype.includes() in all this)

Collapse
matteorigon profile image
Matteo Rigon • Edited

If someone's interested in why it actually works ( as Patrick should have been πŸ˜‰) it's because numbers in javascript are represented as 32 bits in two's complement format.

That means that negative numbers have all bits inverted and then get added 1:

1 = 0000 0000 0000 0000 0000 0000 0000 0001
-1 = 1111 1111 1111 1111 1111 1111 1111 1111
10 = 0000 0000 0000 0000 0000 0000 0000 1010
-10 = 1111 1111 1111 1111 1111 1111 1111 0110

So if you apply the bitwise NOT only -1 can become 0 and thus be falsy

Collapse
bgadrian profile image
Adrian B.G.

It flips all bits of a number, I'm not sure how it exactly works ... So that means, we can take advantage of it and use it in indexOf (haven't seen any other uses yet though).

facepalm, why would you use something you don't know how it works?
As a professional you must guarantee that your code works in all conditions, and you cannot do that if you don't understand how it works.

  1. This snippet I would consider a bad code, most devs will have a hard time understanding it, and that extra time it takes to read&understand will be added for each developer * each time it reads that code. So basically losing resources (time = money).

  2. You do not use an explicit boolean expression, you let the language to cast (all non-zero elements to true), again I think our code must be explicit if (something != 0) would be better than if (something),for us humans.

  3. indexOf is not efficient, it will do a linear search, that means, in worst case, it will:

    • iterate trough all the elements
    • compare each element to your argument
    • probably doing a cast if the types are different

If you want to do that often you can use other data structures that are more efficient for lookups or if the array is sorted a more optimal search algorithm (ex: binary search).

Please keep the "neat" tricks for hackatons, learning projects and meetups, not for production code. We write code for humans, our peers, and bit operators are for machines, we think in decimal.

As for how it works, it has something to do with how the runtimes store the negative numbers

Collapse
werninator profile image
Patrick Werner Author • Edited

Thanks for your opinion and the URL, I'm not trying to convince anyone to use this piece of advice/tip or how you wanna call it - everyone has to decide on his/her own if they want to experiment with it or even use it. I'll use it because my projects mostly consist of legacy code from 2002 that is much worse than a little operator combined with a non-efficient, but established method :c)

Collapse
bgadrian profile image
Adrian B.G.

Do not get me wrong, I commented because I felt that junior devs (that are usually attracted by this kind of posts) should know the other side of the story. You presented the story only for one side so I felt compelled to add the side effects.

Also experimenting is good, I recommend to you and readers:

  • read how and why this trick works
  • test the operator on negative and positive numbers, zero and minimum-maximum numerical values
  • do some benchmarks with jsperf on different methods of searching a specific value in a specific list, including with same types indexOf(1), indexOf("1")
  • learn bit operators, you will not used them often, but when you need them they will bring that extra performance boost, most likely in a critical path of your app

As for legacy code, too bad they don't evolve over time, as our skills and knowledge growth, our code should too.

As other said, Array.includes was added to ECMAScript just because indexOf > -1 was very popular. but, they are not fully interchangeable because they do not use the same comparison algorithm: results of indexOf and includes may be different.

[1,NaN,3].includes(NaN)
true
[1,NaN,3].indexOf(NaN)
-1
Collapse
jarrodconnolly profile image
Jarrod Connolly

That is a great little trick.

It is actually mentioned in the MDN documentation for Bitwise NOT

developer.mozilla.org/en-US/docs/W...

I agree that includes is more readable, but it still must be polyfilled in various situations.

Collapse
webdevinci profile image
webdevinci

I dig it. But I would say there's a reason that you see (function () {.... }()) in people's written code, and !function() { ... } in minified code; minifiers seem to want to do what you have above without care of performance or readability. Perf is minor for most of these if not iterated over, so that's no big deal. Readability across the team is.
Even using something as seemingly basic (and fundamental interview question on truthy/falseyness) as using !!var, I had several people at my new office not understand what it was used for, that it was just a shortcut, and insisted it was causing problems that it wasn't.
I do enjoy these bit operator shortcuts, though... even if I don't fully understand what they are doing

Collapse
cschliesser profile image
Charlie Schliesser

I'd much rather my code be readable and explicit. Converting -1 to falsy and all other indexes to true seems like more work - just check the index that you're looking for, or use another method like Set.has or Array.includes if that's what you're trying to check. My opinion :)

Collapse
jonrandy profile image
Jon Randy

~~ can be used instead of Math.floor and is actually (I believe) faster

Collapse
rcfox profile image
Ryan Fox

~~ truncates, which rounds towards 0. Math.floor rounds towards -∞.

> ~~(-1.2)
-1
> Math.floor(-1.2)
-2
Collapse
jonrandy profile image
Jon Randy

Yup, I should have added that small caveat

Collapse
werninator profile image
Patrick Werner Author

Oh nice! I didn't know that, cool :o