2 Min

A common pattern in object-oriented programming is to define a custom equals method inside the class. You can implement this for comparing instances by checking their internal values:

class Container {
  #name;
  
  constructor(a) {
    this.#name = a;
  }

  valueOf() {
    return this.#name;	
  }

  equals(other) {
    return other instanceof Container && this.valueOf() === other.valueOf();
  }
}

main();

function main() {
  const a = new Container("XYZ");
  const b = new Container("XYZ");
  test(1, a<b, false);                 // 1. PASS
  test(2, a<=b, true);                 // 2. PASS
  test(3, a===b, true);               // 3. FAIL (a and b are different instances)
  test(4, a.equals(b), true);          // 4. PASS (using the custom equals method)
  test(5, a.valueOf()===b.valueOf(), true); // 5. PASS
}

function test(n, a, x) {
  if (a === x) console.log(n + ". PASS");
  else console.log(n + ". FAIL: expected " + x);
}

This way, you can compare two instances using a.equals(b) to check for equality based on the internal values.


In JavaScript, when you’re comparing two class instances using ===, it checks for reference equality, meaning it compares whether both variables refer to the exact same object in memory. This is why a === b in Test 3 fails: a and b are two different instances of the Container class, even though they might hold the same internal value ("XYZ"). The valueOf method is not automatically called in such comparisons.

For Test 4, the expression +a === +b does invoke valueOf() because the unary + operator tries to convert the object into a primitive number (using valueOf). However, since valueOf returns a string ("XYZ"), JavaScript is trying to coerce that string into a number. Since "XYZ" cannot be converted into a number, it results in NaN. Therefore, the comparison NaN === NaN returns false, leading to a failed test.

More ways to compare the values inside the class instances:

Rely on valueOf explicitly for comparisons

You can always use the .valueOf() method in comparisons to extract the internal value explicitly:

test(4, a.valueOf() === b.valueOf(), true); // This works since you're comparing the string values directly.

This approach works but is less elegant than defining an equals method since you have to always remember to call valueOf().

Keep in Mind:

  • Test 3 (a === b) fails because === checks for reference equality, not value equality.
  • Test 4 (+a === +b) fails because +a tries to convert the string "XYZ" into a number, resulting in NaN.
  • The simplest solution is to add an equals method to your class for value comparison, or explicitly use a.valueOf() === b.valueOf() for equality checks.

Bonus: Improving readability using Symbol.toPrimitive

You can also define Symbol.toPrimitive to control how your object is coerced into a primitive (string or number) when necessary:

class Container {
  #name;
  
  constructor(a) {
    this.#name = a;
  }

  valueOf() {
    return this.#name;
  }

  [Symbol.toPrimitive](hint) {
    if (hint === "string") {
      return this.#name; // When coercing to string
    }
    return this.#name;   // Coercing to default (number or string)
  }
}

main();

function main() {
  const a = new Container("XYZ");
  const b = new Container("XYZ");
  test(1, a<b, false);                 // 1. PASS
  test(2, a<=b, true);                 // 2. PASS
  test(3, a===b, true);               // 3. FAIL (a and b still begin different instances)
  test(4, a + '' === b + '', true);    // 4. PASS (String coercion works because of Symbol.toPrimitive)
  test(5, a.valueOf() === b.valueOf(), true); // 5. PASS
}

This allows you to control how the object behaves when coerced into a primitive (like with +a or a + "" for string concatenation).

Updated: