Scott Logic Ltd

Implementing eqeq in JavaScript using eqeqeq

Luke Page, October 26th, 2010

Introduction

So, in a previous post I pointed out some == coercing that was far from obvious. But despite gradually picking up edge cases, I’ve never had a true understanding of the various cases where x == y. So, prompted by Jonathan’s link to the specification explanation and figuring that code is going to be easier to understand than a series of steps.

Delving Into the Spec

So first off, here is how the specification says to do a == comparison.

1. If Type(x) is different from Type(y), go to step 14.
2. If Type(x) is Undefined, return true.
3. If Type(x) is Null, return true.
4. If Type(x) is not Number, go to step 11.
5. If x is NaN, return false.
6. If y is NaN, return false.
7. If x is the same number value as y, return true.
8. If x is +0 and y is -0, return true.
9. If x is -0 and y is +0, return true.
10. Return false.
11. If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and
same characters in corresponding positions). Otherwise, return false.
12. If Type(x) is Boolean, return true if x and y are both true or both false. Otherwise, return false.
13. Return true if x and y refer to the same object or if they refer to objects joined to each other (section 13.1.2).
Otherwise, return false.
14. If x is null and y is undefined, return true.
15. If x is undefined and y is null, return true.
16. If Type(x) is Number and Type(y) is String,
return the result of the comparison x == ToNumber(y).
17. If Type(x) is String and Type(y) is Number,
return the result of the comparison ToNumber(x) == y.
18. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
19. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
20. If Type(x) is either String or Number and Type(y) is Object,
return the result of the comparison x == ToPrimitive(y).
21. If Type(x) is Object and Type(y) is either String or Number,
return the result of the comparison ToPrimitive(x) == y.
22. Return false.

First – The second part of step 13 talks of detecting joined up of functions. However it is just an implementation detail that can even be used or unused. It isn’t necessary to implement detection because === also finds two joined functions to be equivalent.

Second, looking at the implementation of === we can also determine that steps 2 to 13 are already implemeted, so we don’t have to detect +0 and -0 or make NaN !== NaN.

Third I’m going to assume that ToNumber is equivalent to doing using Number(x).

Next we’ll deal with primitive values of objects. The text relating to this is below.

Return a default value for the Object. The default value of an object is
retrieved by calling the internal [[DefaultValue]] method of the object,
passing the optional hint PreferredType. The behaviour of the
[[DefaultValue]] method is defined by this specification for all native
ECMAScript objects (section 8.6.2.6).

For everything apart from Object we return the value, then for objects we retrieve the DefaultValue. This text relating to this is below.

8.6.2.6 [[DefaultValue]] (hint)

When the [[DefaultValue]] method of O is called with hint String, the following steps are taken:
1. Call the [[Get]] method of object O with argument "toString".
2. If Result(1) is not an object, go to step 5.
3. Call the [[Call]] method of Result(1), with O as the this value and an empty argument list.
4. If Result(3) is a primitive value, return Result(3).
5. Call the [[Get]] method of object O with argument "valueOf".
6. If Result(5) is not an object, go to step 9.
7. Call the [[Call]] method of Result(5), with O as the this value and an empty argument list.
8. If Result(7) is a primitive value, return Result(7).
9. Throw a TypeError exception.

When the [[DefaultValue]] method of O is called with hint Number, the following steps are taken:
1. Call the [[Get]] method of object O with argument "valueOf".
2. If Result(1) is not an object, go to step 5.
3. Call the [[Call]] method of Result(1), with O as the this value and an empty argument list.
4. If Result(3) is a primitive value, return Result(3).
5. Call the [[Get]] method of object O with argument "toString".
6. If Result(5) is not an object, go to step 9.
7. Call the [[Call]] method of Result(5), with O as the this value and an empty argument list.
8. If Result(7) is a primitive value, return Result(7).
9. Throw a TypeError exception.

When the [[DefaultValue]] method of O is called with no hint, then it behaves as if the hint were Number, unless O is
a Date object (section 15.9), in which case it behaves as if the hint were String.

The above specification of [[DefaultValue]] for native objects can return only primitive values. If a host object
implements its own [[DefaultValue]] method, it must ensure that its [[DefaultValue]] method can return only primitive
values.

Implementing the JavaScript

So lets implement our own ToPrimitive() (which is mostly an implementation of DefaultValue).

var ToPrimitive = function(o) {
    var primitive,
        funcCalls,
        funcName,
        i;
 
    if  (typeof(o) === "object") {
        if  (o instanceof Date) {
            funcCalls = ["toString", "toValue"];
        } else {
            funcCalls = ["toValue", "toString"];
        }
 
        for(i = 0; i < funcCalls.length; i++) {
            funcName = funcCalls[i];
            if (typeof(o[funcName]) === "function") {
                primitive = o[funcName]();
                if  (typeof(primitive) === "string" ||
                     typeof(primitive) === "number" ||
                     typeof(primitive) === "boolean") {
                    return primitive;
                }
            } 
        }
        throw new Error("Cannot retrieve DefaultValue of Object");
    }
    return o;
};

Next the specification says “If Type(x) is Null” – when typeof(null) returns “object”. It also refers to objects in a way that includes functions, when typeof(function) is “function”. So I’ve created a sanitised TypeOf()…

var TypeOf = function (o) {
    if (o === null) {
        return "null";
    }
    if  (typeof(o) === "function") {
        return "object";
    }
    return typeof(o);
};

And now, our Equals function.

var Equals = function(x, y) {
    var typeX = TypeOf(x),
        typeY = TypeOf(y);
 
    if  (typeX === typeY) { // Step 1
 
        // Steps 2->13 now covered by === since null is of type "null"
        return x === y; // Step 13:
    }
 
    //Step 14, 15
    if  ((x === null || y === null) && 
         (x === undefined || y === undefined)) {
        return true;
    }
    //Step 16
    if  (typeX === "number" && typeY === "string") {
        return Equals(x, Number(y));
    }
    // Step 17
    if  (typeX === "string" && typeY === "number") {
        return Equals(Number(x), y);
    }
    //Step 18
    if  (typeX === "boolean") {
        return Equals(Number(x), y);
    }
    //Step 19
    if  (typeY === "boolean") {
        return Equals(x, Number(y));
    }
    //Step 20
    if  ((typeX === "string" || typeX === "number") && typeY === "object") {
        return Equals(x, ToPrimitive(y));
    }
    //Step 21
    if  (typeX === "object" && (typeY === "string" || typeY === "number")) {
        return Equals(ToPrimitive(x), y);
    }
    //Step 22.. Not convinced this will ever happen
    return false;
};

Unit Tests

And finally, rather than put more guess what's == paragraphs, I present some unit tests...

Test == Equals


This entry was posted on Tuesday, October 26th, 2010 and is filed under Blog.

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site .


4 Responses to “Implementing eqeq in JavaScript using eqeqeq”

  1. Martin Rock-Evans says:

    Interesting stuff. I love that new Date(2000) != new Date(2000), but new Date(2000) == new Date(2000).toString().

    It’s worth noting that this is the case for all objects, so two objects of the same type are not ==, but two objects of different types may be == depending on if/how toString and toValue have been implemented.

    I think the implementation of ToPrimative should throw an error if it can’t convert to a primative, otherwise there is the danger that “” == obj, where obj.toString=function(){return this;} would go into an infinte loop. (See step 9 of DefaultValue)

    e.g.
    var obj = {toString: function(){ return this; }};
    if (“”==obj) alert(“equals”) else alert (“not equals”);

  2. I’ve created a little tool that should help people visualize what’s going on. Which conversion steps are taken. You can find it on http://jscoercion.qfox.nl :)

    Also, the + operator (not addition, but the unary operator) is defined to explicitly execute the abstract ToNumber function. But Number(x) is fine too, the edge case where it might differ (returning 0 when no argument is given) can never occur in your code :) But + is likely to be slightly (!) faster. So if speed is any concern… :p

  3. matt says:

    Excellent information, and well written. Thisprogramming information is really helpful thanks!

Leave a Reply

© 2012 Scott Logic Ltd. All Rights Reserved.