Episode 1 — Fundamentals / 1.24 — Object Methods

1.24.e — Object.is()

In one sentence: Object.is(value1, value2) performs a same-value equality check that fixes the two edge cases where === gives surprising results: NaN and signed zeros.

Navigation: <- 1.24.d — Object.fromEntries . 1.24.f — Object.keys ->


1. What does Object.is() do?

Object.is() compares two values and returns true if they are the same value. It behaves like === (strict equality) with two important corrections:

// Where === gets it "wrong" (per mathematical intuition):
NaN === NaN;      // false  -- surprising!
+0 === -0;        // true   -- hides sign information

// Object.is fixes both:
Object.is(NaN, NaN);   // true   -- NaN IS the same as NaN
Object.is(+0, -0);     // false  -- +0 and -0 ARE different values

2. Syntax

Object.is(value1, value2)
ParameterDescription
value1First value to compare
value2Second value to compare
Returnsbooleantrue if the values are the same value

3. The three equality operators compared

Comprehensive comparison table

Expression== (loose)=== (strict)Object.is()
1, 1truetruetrue
1, "1"truefalsefalse
0, falsetruefalsefalse
null, undefinedtruefalsefalse
null, nulltruetruetrue
undefined, undefinedtruetruetrue
NaN, NaNfalsefalsetrue
+0, -0truetruefalse
+0, 0truetruetrue
"", falsetruefalsefalse
[], 0truefalsefalse
{}, {}falsefalsefalse

Summary of differences

OperatorType coercion?NaN === NaN+0 === -0
== (loose)Yesfalsetrue
=== (strict)Nofalsetrue
Object.is()Notruefalse

4. Understanding the NaN fix

In IEEE 754 floating-point math, NaN is defined as not equal to anything, including itself. This is the standard behavior that === follows. But logically, if you have two NaN values, they represent the same concept.

const a = NaN;
const b = NaN;

// === says they're different
console.log(a === b);          // false

// Object.is says they're the same
console.log(Object.is(a, b));  // true

// Common NaN-producing operations
console.log(Object.is(0 / 0, NaN));               // true
console.log(Object.is(Math.sqrt(-1), NaN));        // true
console.log(Object.is(parseInt("abc"), NaN));      // true
console.log(Object.is(Number("hello"), NaN));      // true

Before Object.is: checking for NaN

// Old ways to check for NaN:
Number.isNaN(value);          // recommended (ES6)
value !== value;              // classic trick (only NaN is not equal to itself)

// Object.is approach:
Object.is(value, NaN);        // also works

5. Understanding the signed-zero fix

JavaScript has both +0 and -0. They behave identically in almost all operations, but they are technically different values.

console.log(+0 === -0);          // true  -- === hides the difference
console.log(Object.is(+0, -0));  // false -- Object.is reveals it

// When does -0 appear?
console.log(-1 * 0);    // -0
console.log(0 / -1);    // -0
console.log(-0);         // -0

// They look the same in most output
console.log(String(-0));        // "0"
console.log(-0 === 0);          // true
console.log(-0 + "");           // "0"

// But they differ in:
console.log(1 / +0);   // Infinity
console.log(1 / -0);   // -Infinity
console.log(Object.is(1 / +0, 1 / -0));  // false (Infinity vs -Infinity)

When signed zeros matter

  • Mathematical computations — direction of approach in limits
  • Coordinate systems — distinguishing direction of movement along an axis
  • Rarely in everyday web development

6. When to use Object.is()

Most code should use ===. Reach for Object.is() in specific scenarios:

Checking for NaN in comparisons

function updateIfChanged(oldVal, newVal) {
  // === would say NaN !== NaN, causing unnecessary updates
  if (!Object.is(oldVal, newVal)) {
    console.log(`Changed from ${oldVal} to ${newVal}`);
  }
}

updateIfChanged(NaN, NaN);  // no log -- correctly sees them as equal
updateIfChanged(5, 5);      // no log
updateIfChanged(5, 10);     // "Changed from 5 to 10"

Building a generic comparison utility

function areSameValue(a, b) {
  return Object.is(a, b);
}

// Handles all edge cases correctly
console.log(areSameValue(NaN, NaN));   // true
console.log(areSameValue(+0, -0));     // false
console.log(areSameValue(1, 1));       // true
console.log(areSameValue({}, {}));     // false (different references)

Implementing a Set-like deduplication that handles NaN

function uniqueValues(arr) {
  return arr.reduce((acc, val) => {
    if (!acc.some(existing => Object.is(existing, val))) {
      acc.push(val);
    }
    return acc;
  }, []);
}

console.log(uniqueValues([1, NaN, 2, NaN, 3, 1]));
// [1, NaN, 2, 3]  -- only one NaN kept

7. React uses Object.is internally

React's useState and useEffect use Object.is for state comparison to determine if a re-render is needed:

// React's internal comparison (simplified):
function stateHasChanged(oldState, newState) {
  return !Object.is(oldState, newState);
}

// This is why:
const [count, setCount] = useState(0);
setCount(0);  // No re-render! Object.is(0, 0) === true

// And why NaN state works:
const [val, setVal] = useState(NaN);
setVal(NaN);  // No re-render! Object.is(NaN, NaN) === true
// (with === this would trigger infinite re-renders)

This is documented in React's source and is a practical reason to understand Object.is.


8. Polyfill

Understanding the polyfill helps solidify the concept:

// Polyfill (for understanding)
if (!Object.is) {
  Object.is = function (x, y) {
    if (x === y) {
      // +0 !== -0 check:
      // 1/+0 = Infinity, 1/-0 = -Infinity
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // NaN === NaN check:
      // NaN is the only value not equal to itself
      return x !== x && y !== y;
    }
  };
}

The polyfill reveals the two tricks:

  1. If x === y is true but x is 0, check if 1/x === 1/y (catches +0 vs -0)
  2. If x === y is false, check if both are NaN (x !== x is only true for NaN)

9. Object.is with objects and references

Object.is checks reference identity for objects, just like ===:

const obj = { a: 1 };
const same = obj;
const clone = { a: 1 };

Object.is(obj, same);    // true  -- same reference
Object.is(obj, clone);   // false -- different objects, even with same content
Object.is([], []);        // false
Object.is({}, {});        // false

It is not a deep equality check. For deep comparison, use libraries or write a recursive comparator.


10. Comparison with other NaN-checking methods

MethodNaN detection+0 vs -0
x === x (false means NaN)Yes (inverted)No distinction
isNaN(x) (global)Yes, but coerces firstN/A
Number.isNaN(x)Yes, no coercionN/A
Object.is(x, NaN)Yes, no coercionN/A
Object.is(x, y)Handles NaN equalityDistinguishes signs
// Global isNaN coerces -- dangerous:
isNaN("hello");         // true!  (coerces to NaN first)
Number.isNaN("hello");  // false  (no coercion)
Object.is("hello", NaN); // false (no coercion)

11. Edge cases

// Symbols
const s = Symbol("id");
Object.is(s, s);                    // true
Object.is(Symbol("id"), Symbol("id")); // false (unique symbols)

// null and undefined
Object.is(null, null);              // true
Object.is(undefined, undefined);    // true
Object.is(null, undefined);         // false (unlike ==)

// Infinity
Object.is(Infinity, Infinity);     // true
Object.is(-Infinity, -Infinity);   // true
Object.is(Infinity, -Infinity);    // false

// Booleans
Object.is(true, true);             // true
Object.is(true, 1);                // false (no coercion)

Key takeaways

  1. Object.is(a, b) is like === but NaN equals NaN and +0 does not equal -0.
  2. Most code uses ===Object.is is for specific edge cases involving NaN or signed zeros.
  3. React uses Object.is internally for state change detection.
  4. For objects, Object.is checks reference identity, not deep equality.
  5. The polyfill uses two tricks: 1/x === 1/y for zero signs and x !== x for NaN detection.

Explain-It Challenge

Explain without notes:

  1. What does Object.is(NaN, NaN) return? How does this differ from NaN === NaN?
  2. Why would React choose Object.is over === for state comparison?
  3. Can Object.is tell you if two objects have the same contents? Why or why not?

Navigation: <- 1.24.d — Object.fromEntries . 1.24.f — Object.keys ->