Let’s understand how can we make (a===1 && a===2 && a===3) to ever be true with help of getter-setter descriptors.

We shall take a brief dive into the traditional problem and also solve a problem which is extension of it.

Table of contents

Revisiting (a==1 && a==2 && a==3) (loose equality) problem

If you are already familiar with this question and understand how can one solve this JavaScript tricky riddle (Yes, riddle, I don’t see a use case in production code, ¯\_(ツ)_/¯ ), you can move on to next section which is an attempt to solve the extension of it (with strict equality). There is also a reddit discussion around this problem. Most interesting comment which I noticed is,

“If this is the type of code I’m likely to encounter in your codebase, then I’m out”.


Problem (a==1 && a==2 && a==3)

Can (a==1 && a==2 && a==3) ever evaluate to true?

Yes, and to make it true one can do this,

const a = { value : 0 };
a.valueOf = function() {
    return this.value += 1;
};

console.log(a==1 && a==2 && a==3); //true

The purpose of a question like this in interview, isn’t to know the answer to the brain-teaser, so much as to get a feel for how the interviewee thinks through problems, and whether they have awareness of the kinds of features and oddities of JS that can make the == comparitor behave strangely.

Explanation

The secret here is, loose equality operator (==).

In JS, loose equality compares two values for equality, after converting both values to a common type. After conversions (one or both sides may undergo conversions), the final equality comparison is performed exactly as === performs it. Loose equality is symmetric: A == B always has identical semantics to B == A for any values of A and B (except for the order of applied conversions). Refer here for all in-depth explanation about loose and strict comparison.

Question here is how does JavaScript coerce this values?

Based on values of comparison, type coercion occurs, lets consider a internal function to convert so,

ToPrimitive(input, PreferredType?)

The optional parameter PreferredType indicates the final type of the conversion: it is either Number or String, depending on whether the result of ToPrimitive() will be converted to a number or a string.

Conversion happens in following order,

  1. If input is primitive type, return it
  2. If input is an object. Call input.valueOf(). If the result is primitive, return it.
  3. Else, call input.toString(). If the result is primitive, return it.
  4. throw a TypeError (indicating the failure to convert input to a primitive).

If PreferredType is Number, the above algorithm works in specified order. If PreferredType is String, steps 2 and 3 are swapped. The PreferredType can also be omitted; it is then considered to be String for dates and Number for all other values. The default implementation of valueOf() returns this, while the default implementation of toString() returns type information.

This is how the operators + and == call ToPrimitive(). (Ahaa! )

So in above code, as soon as JS saw, a==1, ‘1’ being primitive type it tried to convert ‘a’ to Number, and with above algorithm, a.valueOf was called returning ‘1’ (incrementing previous value for and returning it). Similar coercion came into effect for a==2 and a==3 thus incrementing it for next time.


Problem (a===1 && a===2 && a===3) (strict comparison)

Can (a===1 && a===2 && a===3) ever evaluate to true?

Yes, below code would make this true,

var value = 0; //window.value
Object.defineProperty(window, 'a', {
    get: function() {
        return this.value += 1;
    }
});

console.log(a===1 && a===2 && a===3) /true

Explanation

Our understanding from previous problem is, primitive values would never satisfy above condition, we need by some means call a function and inside that function we can perform this magic. But calling a function would involve () after function name. hmmm. strange. But since there is no loose equality, .valueOf won’t be called by JS Engine, bringing function Property, especially getter, to the rescue.

What are property descriptors?

A property descriptor can be of two types: data descriptor, or accessor descriptor.

  1. Data descriptor

    Mandatory properties:

    • value

    Optional properties:

    • configurable
    • enumerable
    • writable

    Sample:

    {
        value: 5,
        writable: true
    }
    
  2. Accessor descriptor

    Mandatory properties:

    • Either get or set or both

    Optional properties:

    • configurable
    • enumerable

    Sample:

    {
        get: function () { return 5; },
        enumerable: true
    }
    

Accessor Example from Mozilla pages,

    // Example of an object property added
    // with defineProperty with an accessor property descriptor

    var bValue = 38;

    Object.defineProperty(o, 'b', {
        // Using shorthand method names (ES2015 feature).
        // This is equivalent to:
        // get: function() { return bValue; },
        // set: function(newValue) { bValue = newValue; },
        get() { return bValue; },
        set(newValue) { bValue = newValue; },
        enumerable: true,
        configurable: true
    });
    o.b; // 38
    // 'b' property exists in the o object and its value is 38
    // The value of o.b is now always identical to bValue,
    // unless o.b is redefined

A property on a object be defined by using Object.defineProperty as mentioned in the solution. You can dig into syntax and definition here . Interestingly, get and set are accessors which can be called via dot(.) operator, i.e if object a has getter property called b then it is called just like any other value with dot notation, viz, a.b. This is the solution to our initial problem, wherein we needed to call a function without (), with get property, we are able to call a function without using () after function name.

In above mentioned solution, we are defining a getter property on window object, so a is directly accessible in code (global variables) and hence are able to achieve the result. If we define a property called a on some other object than window, say object1, we need to change the condition to object1.a===1 && object1.a===2 && object1.a===3.

Github Gist

References