DEV Community

EmNudge
EmNudge

Posted on

(a == 1 && a == 2 && a == 3) === true - Wait, hold on...

Some of you may recognize the problem in the title. It's a bit of a famous sample of wtfJS, explained very well by Brandon Morelli in 2018.

The code sample is as follows:

    if (a == 1 && a == 2 && a == 3) {
        console.log("What?!");
    }
    // logs: What?!
Enter fullscreen mode Exit fullscreen mode

Why does it work? Well the trick is in realizing that a here is not a primitive. It's an object with a getter - pretending to be a primitive.

So what happens when we even try to compare an object and a primitive? If we take a look at the spec it says (by rules 8 & 9) that we try to coerce the object into a primitive. How? Via ToPrimitive, a function defined in the spec as well.

In short, it tries to coerce the object into a number. If that doesn't work, it tries to coerce it into a string. Let's try to coerce an object to a string and number.

    const num = Number({});
    console.log(num); // NaN

    const str = String({});
    console.log(str); // [object Object]
Enter fullscreen mode Exit fullscreen mode

Okay, so neither of those are helpful. How exactly is it coercing them?

According to the spec, it's calling .valueOf to get the number and .toString to get the string. If .valueOf returns an object, it moves on to .toString. If .toString doesn't return a primitive, it will actually throw an error: Uncaught TypeError: Cannot convert object to primitive value.

We can override them ourselves like so:

    const a = {
        valueOf()  {
            return 55;
        },
        toString() {
            return 100;
        }
    };

    if (55 == a) console.log("we got valueOf()!");
    if (100 == a) console.log("we got toString()!");
    // logs: we got valueOf()!

    // returning an object, so it will be skipped
    a.valueOf = function() { return {} };

    if (55 == a) console.log("we got valueOf()!");
    if (100 == a) console.log("we got toString()!");
    // logs: we got toString()!
Enter fullscreen mode Exit fullscreen mode

You see, we don't actually have to return a string or number in either.

So how do we use this to solve our problem? We make one of the getters return a value and increment it.

    const a = {
        val: 0,
        valueOf()  {
            this.val++;
            console.log("value incremented!");
            return this.val;
        }
    };

    if (a == 1 && a == 2 && a == 3) {
        console.log("We got it!");
    }

    // logs:
    // value incremented!
    // value incremented!
    // value incremented!
    // We got it!
Enter fullscreen mode Exit fullscreen mode

We can do something similarly with the Proxy class, but taking advantage of the same concept.

const a = new Proxy({ value: 1 }, {
    get(obj, prop) {
        if (prop !== 'valueOf') return obj[prop];
        return () => obj.value++;
    }
})
Enter fullscreen mode Exit fullscreen mode

I won't go too into Proxy in this article, as Keith Cirkel made a much better article on the subject over here.

In essence, we are defining a new object with a getter "trap" that returns the current value property and increments it if its .valueOf() method is called. This is just a fancier way of doing something that we did much simpler just before.

Regardless, is this impossible using strict equality? What if we were presented with the same example, but with triple equals?

Strict Equality

Well actually, it's possible. But first, we have to nail down a few fundamentals.

The first being the window object. Any property on this object is automatically given to us as if it were defined in some global scope. As such, window.parseInt is the same as just parseInt, window.alert is the same as just alert, and so on.

We can also define our own properties and seemingly create variables dynamically.

    function makeVariables() {
        window.foo = 55;
        window.bar = "hello";
    }
    makeVariables()

    if (foo) console.log(foo);
    if (bar) console.log(bar);
    if (baz) console.log(baz);

    // logs:
    // 55
    // "hello"
    // Uncaught ReferenceError: baz is not defined
Enter fullscreen mode Exit fullscreen mode

Side note - this is a bad idea. Don't do this. But we're going to need this for our own example.

Next, we need to go over Object.defineProperty. This function lets us define properties on objects with unique qualities. It feels new, but it actually works on IE9.

This cool method lets us make a property really constant, so people don't change it. It also lets us define a custom getter method! Things are starting to feel a bit familiar!

    const myObj = {}
    Object.defineProperty(myObj, 'val', {
        get() {
            return Math.random();
        }
    })

    console.log(myObj.val);
    console.log(myObj.val);
    console.log(myObj.val);

    // logs:
    // 0.6492479252057994
    // 0.6033118630593071
    // 0.6033118630593071
Enter fullscreen mode Exit fullscreen mode

Why is this better than the previous method? Well this time we don't have to rely on coercion!

Let's combine the 2 things we just discussed to finalize our second example:

    let value = 0;
    Object.defineProperty(window, 'a', {
        get() {
            value++;
            console.log("value incremented!");
            return value;
        }
    })

    if (a === 1 && a === 2 && a === 3) {
        console.log("We got it!");
    }

    // logs:
    // value incremented!
    // value incremented!
    // value incremented!
    // We got it!
Enter fullscreen mode Exit fullscreen mode

Nice! Now we have it working with strict equality!

We unfortunately can't define a variable in the object itself (and then access it in the getter), but if we really don't want to pollute the scope, we can use closures and IIFEs in a really tricky way (credit goes to P35 from the SpeakJS discord server).

    Object.defineProperty(window, 'a', (function(){
        let value = 0;
        return {
            get() {
                value++;
                console.log("value incremented!");
                return value;
            } 
        }
    })());
Enter fullscreen mode Exit fullscreen mode

but this obviously is a pretty messy example.

What about Proxy? Could we use it here? Unfortunately, Proxy will not work with the window object, so it doesn't help us in this instance.

Wrap Up

So when is this useful? Hardly ever.

Well, there are some occasions. Have you ever encountered a really weird error when using a JS framework? Something like Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute?

Your framework is probably using Proxies and getter methods under the hood. They're useful, but only when things get complicated and you want to hide that underlying complexity.

Top comments (3)

Collapse
 
notwearingpants profile image
NotWearingPants

There is no reason to use an IIFE, certainly not one that returns anything.
Since we are using a block scoped variable we can just wrap it in curly braces:

{
    let value = 0;
    Object.defineProperty(window, 'a', {
        get() {
            value++;
            console.log("value incremented!");
            return value;
        }
    });
}

if (a === 1 && a === 2 && a === 3) {
    console.log("We got it!");
}
Collapse
 
emnudge profile image
EmNudge

Yes, that's true!
I was thinking more along the lines of having the scope reside entirely within Object.defineProperty where I don't believe a block scope would actually work, which is why I used an IIFE.
But this is completely valid, too!

Collapse
 
emnudge profile image
EmNudge

Haha, that would have been some real weirdness. It still works, but you need some code before it.

I do have a different article, the one on wtfjs and coercion, that is just copy/paste though!