Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Understanding Loose Equality In JavaScript
EmNudge
EmNudge

Posted on

     

Understanding Loose Equality In JavaScript

For those who prefer a more audiovisual form, a video almost identical to the article can be seen over here:

Abstract Equality, or as I've titled this article "Loose Equality" is (I think) one of the most misunderstood topics in JavaScript. People know loose equality, the double equals (==), to check if its operands are roughly equal to each other. The string"55" and the number55 arekind of the same thing, but notstrictly the same thing, with triple equals (===).

People usually advise against using loose equality. Personally? Well if JavaScript came out with a**strict* strict mode* that removed loose equality, I wouldn't be too bothered.

But there's a lot of misinformation out there and I thought it would be helpful to clean some of that up. Which is why I've been working on this topic for so long.

// loose equality vs strict equality"55"==55// -> true"55"===55// -> false
Enter fullscreen modeExit fullscreen mode

Loose equality, in reality, is a process that tries toimplicitly coerce its operands to be the same type before passing it off to strict equal to give you the real result.Implicit coercion by itself actually isn't too bad. It's used in many other languages and JavaScript programmers use it pretty often.

In this example, we take advantage offalsy and truthy values to check whether we should print out an array to the console. If the array exists and has a length property greater than 0, print it out.

// example of implicit coercionconstmyArr=[1,2,3,4,5];if(myArr&&myArr.length){console.log("My arr is:"+myArr);}
Enter fullscreen modeExit fullscreen mode

Falsy values include all the JavaScript values that will evaluate tofalse when converted into a boolean.

Boolean('')// -> falseBoolean(0)// -> falseBoolean(0n)// -> falseBoolean(NaN)// -> falseBoolean(null)// -> falseBoolean(undefined)// -> false
Enter fullscreen modeExit fullscreen mode

Do not confuse this with abstract equality, however. Double equals often doesnot rely on this system whatsoever. While using the exact same values, we get true for half only. I'm no statistician, but 50-50 looks like zero correlation to me.

false==''// -> truefalse==0// -> truefalse==0n// -> truefalse==NaN// -> falsefalse==null// -> falsefalse==undefined// -> false
Enter fullscreen modeExit fullscreen mode

In fact, I would go so far as to say the concept of falsy valuesnever comes up withinabstract equality in the spec? What's the spec?

The JavaScript specification is an esoteric document that instructs browsers on how JavaScript should work. Browsers all can code up the implementation themselves, but if you want to know how JavaScript works without digging through C++ code, this is the best place to look.

The spec can often be pretty confusing, butthis particular section is actually kind of readable. It defines abstract equality as a list of steps and I think it's pretty cool. If you're ever wondering why null is loosely equal to undefined, this is why. Because it says so. There is no low-level reason why it must be that way - the discussion stops here. It works that way because the document says it should.

While I can go through the document, I'm going to instead use a tool I've been working on to explain it a bit more simply -The Abstract Equality Stepper. I've written up the steps to roughly match spec. There are some minor changes in formatting to help with how my tool works, but it's essentially the same.

preview of tool

Let's punch in some examples we've just shown to explore how this works.false and0 perhaps.

Untitled (1)

(View it here)

We can see that it declares either of the operands are a boolean, we convert the boolean to a number. Always. No matter what the other value is.

Notice that it tells us to perform an abstract equality comparison, but these are the steps thatdefine what an abstract equality comparisonis. That's right, this is recursion. We restart with new values. Since the types are now equal, we chuck it off to a strict equality comparison which returns true since they're the same value.

Notice that abstract equalityuses strict equality.
Untitled (2)

So technically abstract equality must be less performant if the implementation matches the spec exactly. This is way too minor to matter in practice, but I thought it was interesting.

Let's tryfalse and''. We convert the boolean to a number like last time, but now we're left with a number versus a string.

Untitled (3)

(View it here)

We convert the string to a number and then go to strict equality. We're converting to numbers a lot here. It's for good reason. Numbers can be thought of as the most primitive type. It's easy to compare number to number and it's essentially what we're doing when we compare anything else. Even when we compare using reference equality (like with 2 objects) we're comparing memory locations, which, as you might have guessed, are numbers.

We can substitute0 for false for all of the other examples.

0==NaN// -> false0==null// -> false0==undefined// -> false
Enter fullscreen modeExit fullscreen mode

0 isn'tNaN so that's gonna be false. And then there is no step to define0 andnull orundefined, so we getfalse bydefault.

Untitled (4)

Nothing to do with falsy values here. Just looking at steps and following the rules.

With that out of the way, let's look at a common example of abstract equality weirdness - a real headscratcher.

WTFJS - The Headscratcher

![]==[]// -> true
Enter fullscreen modeExit fullscreen mode

Thislooks paradoxical, but it actually makes sense. First, we convert the left array to a boolean. Thisdoes involve the concept of falsy, but we haven't touched abstract equality yet, just expression evaluation. Since arrays aren't falsy, we would gettrue, but we're using an exclamation mark, so we flip that and getfalse.

false==[]
Enter fullscreen modeExit fullscreen mode

Since booleans always turn to numbers in this system, our operands are0 and[]. Now what?

Well, now we find ourselves face to face with the magicalToPrimitive. This one is interesting. We can't just compare a primitive value and an object, we need 2 primitive values or 2 objects. We try turning our array into a primitive and out pops an empty string.

(Note: a function is just a callable object. When we use the termobject, we include functions)

0 and'' means we turn the string into a number, which leads us to0 and0 which are equal.

But how doesToPrimitive work? What does it do?

We canlook at the spec again, but this time it's a little more difficult, so I've taken the liberty of converting it to plain JavaScript.

If we're passed a primitive value, just return that. No need to convert a primitive to a primitive.

Then we check for aSymbol.toPrimitive property. This is a rather recent addition to JavaScript which allows you to define theToPrimitive behavior a bit more easily.

Untitled (4)

If such a method exists, we try to convert it to a number. How? We check for a.valueOf property, which is whatNumber calls. If you try adding your object to a number, it will try to look for this property and call it.

If this property doesn't exist on your object or it itself returns an object, we try converting it to a string. Using, of course, the.toString property. This is actually defined on all object by default, including arrays. If you don't touch your object thenToPrimitive will return a string. For arrays, this means returning all its values as a comma-separated list. If it's empty, that's an empty string.

constobj={valueOf(){console.log('calling valueOf');return100;},toString(){console.log('calling toString');return'👀';}};console.log(obj+43);console.log(`I see you${obj}`);
Enter fullscreen modeExit fullscreen mode

(Note: string concatenation itself doesn't always call.toString)

And there's your explanation!

But if you look a bit closer, you'll notice a few errors being thrown. Wait, does that mean...

Yup! There are often times where just using double equals will throw an error instead of returning false. Let's create such a scenario right now.

Throwing Errors With Equality Checks

constobj1={[Symbol.toPrimitive]:45};console.log(obj1==45);// Uncaught TypeError: number 45 is not a function
Enter fullscreen modeExit fullscreen mode

We can also just make it a function, but return an object.

constobj2={[Symbol.toPrimitive]:()=>Object()};console.log(obj2==45);// Uncaught TypeError: Cannot convert object to primitive value
Enter fullscreen modeExit fullscreen mode

Or do the same with the other methods

constobj3={toString:()=>Object(),valueOf:()=>Object()};console.log(obj3==45);// Uncaught TypeError: Cannot convert object to primitive value
Enter fullscreen modeExit fullscreen mode

Now, we can't actually delete these methods on most objects. I mentioned earlier that all objects implement this by default. All objects of course inherit this method from the object prototype and we can't really delete that.

However, it's possible to make an object with no prototype usingObject.create(null). Since it has no prototype, it has novalueOf() and notoString() and thus it will throw an error if we compare it to a primitive. Magical!

Object.create(null)==45// Uncaught TypeError: Cannot convert object to primitive value
Enter fullscreen modeExit fullscreen mode

With that detour, let's close with the essence of this article - how to understand loose equality.

Conclusion

When comparing 2 things of different types, it'll help to convert the more complex type to a simpler representation. If we can convert to a number, do that. If we're adding an object to the mix, get the primitive value and again try squeezing a number out of it.

null andundefined are loosely equal and that's that.

If we get something likeSymbol() or we comparenull orundefined with anything else by each other, we getfalseby default.Symbol() actually has a.toString() method, but it doesn't really matter. The spec says we getfalse, so we getfalse.

If we want to describe the steps in a bit of a simpler form, it looks something like this:

  1. null equals undefined
  2. Number(string) == number
  3. BigInt(string) == bigint
  4. Number(boolean) == anything
  5. ToPrimitive(object) == anything
  6. BigInt(number) == bigint
  7. false

Stay curious!

Top comments(5)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
ashvin777 profile image
Ashvin Kumar Suthar
Founder of JSitor.com. Enjoys working in JS, CSS, and HTML.
  • Location
    India
  • Work
    Creator of JSitor.com at -
  • Joined

A suggestion, if you want to see jsitor snipepts embedded here, you can use jsitor liquid tag. Its already supported. Check the dev.to liquid tag section for details.

CollapseExpand
 
emnudge profile image
EmNudge
web man
  • Location
    NYC
  • Work
    Front-end developer
  • Joined
• Edited on• Edited

I didn't, but that sounds like a good idea, so I've gone ahead and converted it to a liquid tag!

While I have you here, however, are there any plans to add a tsConfig.json file to jsitor? I had to abandon using TS for a couple of REPLs because it wouldn't allow me to use modern JS syntax without a bunch of
// @ts-ignore
being thrown around the place.

CollapseExpand
 
ashvin777 profile image
Ashvin Kumar Suthar
Founder of JSitor.com. Enjoys working in JS, CSS, and HTML.
  • Location
    India
  • Work
    Creator of JSitor.com at -
  • Joined

We didn't have any plan yet to add a config file, however, we can allow modern JS syntax if anything is missing and make it configurable if required.

Is there any particular syntax with which you are looking for that is not working? Please let us know.
You can also report any bugs or requests on our Github issues page -github.com/jsitor/jsitor/issues/ne...

Thread Thread
 
emnudge profile image
EmNudge
web man
  • Location
    NYC
  • Work
    Front-end developer
  • Joined

.includes() andBigInt() are the ones that have been specifically annoying me when making recent snippets, but I'm sure there are more that I just haven't come across yet.
Using symbols as index values might just not be allowed in TS at all, so idk if there's a good solution to that.
i.e.obj[Symbol.toPrimitive] requiresany as an index or just// @ts-ignore

CollapseExpand
 
thomasfe profile image
thomas
  • Joined

Awesome explanation of loose equality in JavaScript! The examples make it really easy to grasp how == behaves with type coercion. For anyone interested in exploring both loose and strict equality in more depth, I highly recommendblog post about javascript equality operators. It covers additional practical use cases and tips to navigate these operators effectively. 😊

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

web man
  • Location
    NYC
  • Work
    Front-end developer
  • Joined

More fromEmNudge

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp