π― A simple way to test for equality in class instances
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 inNaN
. - The simplest solution is to add an
equals
method to your class for value comparison, or explicitly usea.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).