Equality & sameness
Compare JavaScript values with confidence by learning ==, ===, Object.is, and SameValueZero step by step.
- 01Pick the right checkKnow which set of rules ===, ==, Object.is, includes, Map, and Set each use, and when their answers differ.
- 02Follow loose equality step by stepWalk through the steps of == in order, instead of memorizing a big table.
- 03Handle NaN and -0Spot the two number cases that explain most surprising equality results.
Four ways to check sameness
When you compare two values, you ask one question: are these the same? JavaScript has four different sets of rules for answering it. Each set of rules is a fixed list of steps. The specification, the official rulebook for JavaScript, calls such a list an algorithm.
You met ===, ==, NaN, and the null/undefined rule in earlier lessons. This lesson puts all four sets of rules side by side. Then you can predict each answer instead of memorizing a big table.
Picture four judges at a door. Each one decides whether two ID badges belong to the same person. The first judge accepts only an exact match. The second judge translates the badges first. The third judge uses a microscope. The fourth judge checks a guest list. Give them the same two badges, and they can give different answers, because each one follows a different rulebook.
- In real life: A strict judge accepts only an exact match
- In JavaScript:
===: same type and same value, nothing changed - In real life: A translator judge translates the badges first
- In JavaScript:
==: may convert the values, then compares - In real life: A microscope judge notices tiny details
- In JavaScript:
Object.is: noticesNaNand the minus sign on-0 - In real life: A guest-list judge checks if a name is on the list
- In JavaScript: SameValueZero: used by
includes,Map, andSet
The specification gives each set of rules a name:
- IsStrictlyEqual is used by
===. - IsLooselyEqual is used by
==. - SameValue is used by
Object.is. - SameValueZero is used by
includes,Map, andSet.
SameValueZero is SameValue with one change: it counts -0 (negative zero) and +0 (positive zero) as the same.
The table below compares the four. Read each row as: "these rules, used here, give these answers."
| Rules | Where you use them | Converts values first? | Is NaN the same as NaN? | Is -0 the same as +0? |
|---|---|---|---|---|
| Strict equality | === and !== | No | No | Yes |
| Loose equality | == and != | Yes | No | Yes |
| SameValue | Object.is | No | Yes | No |
| SameValueZero | includes, Map, Set | No | Yes | Yes |
Strict equality: same type, same value
INTERACTIVEStrict equality, written ===, is the one to use by default. It never converts either value. It checks like this:
- Are both values the same type? If not, the answer is
falseright away. - Are they primitives, the simple values like numbers, strings, and booleans? Then it checks that the values are the same.
- Are they objects, arrays, or functions? Then it checks that both sides point to the very same object.
A variable never holds an object itself. It holds a reference: a note that says where the object lives. So for objects, === compares the two references.
const user = "Asha";const sameName = "Asha"; console.log(user === sameName);console.log(3 === "3");console.log(true === 1);Line 4 prints true, because both variables hold the same string, "Asha". Line 5 prints false, because the string "3" and the number 3 are different types. Line 6 also prints false. true is a boolean and 1 is a number, so they are different types too.
const shared = { id: 1 };const alsoShared = shared;const twin = { id: 1 }; console.log(1 === "1");console.log(shared === alsoShared);console.log(shared === twin);console.log(NaN === NaN);console.log(-0 === +0);Lines 5 to 7 follow the rules above:
- Line 5 prints
false: a number and a string are different types. - Line 6 prints
true:sharedandalsoSharedpoint to the same object. - Line 7 prints
false:twinlooks the same, but it is a separate object.
Lines 8 and 9 are the two surprises:
- Line 8 prints
false.NaNmeans "Not a Number". It is the result of a number calculation that failed. By the old number rules,NaNis not equal to anything, not even itself. - Line 9 prints
true. JavaScript has a negative zero,-0, and a positive zero,+0. Strict equality treats them as the same zero.
The playground below asks all four sets of rules about these cases, and about strings, numbers, BigInts, arrays, and objects too.
const firstCart = { total: 3 };const savedCart = firstCart;const otherCart = { total: 3 }; console.log(firstCart === savedCart);console.log(firstCart === otherCart);Line 2 does not copy the cart. It copies the reference, so savedCart points to the same cart as firstCart. Line 5 prints true. Line 3 makes a brand-new cart with the same contents. Line 6 prints false, because it is a different object.
firstCart and savedCart point to one cart. otherCart points to a second cart that only looks the same. === asks: do both names point to the same cart?=== never looks inside two objects to compare their properties. If you want to know whether two carts hold the same things, compare the properties yourself, like firstCart.total === otherCart.total.
const a = NaN;const b = NaN;console.log(a === b);console.log(a == b);console.log(Object.is(a, b));console.log([a].includes(b));=== has an old rule: NaN is not equal to anything, not even NaN.
The types match, so == works just like ===.
SameValue says NaN is the same as NaN.
SameValueZero says NaN matches NaN.
You picked NaN vs NaN. Each answer below comes from running real JavaScript.
Use === unless you can say exactly which conversion you want. With === the question stays simple: the type and the value match, or they don't.
Loose equality, step by step
STEP THROUGHLoose equality, written ==, is not random. It follows a fixed list of steps. At each step it asks: "Are the two types the same yet? If not, is there a rule to convert one of them?"
To convert a value means to make a new value of a different type from it, like turning the text "5" into the number 5. The Type coercion lesson showed the spec's steps for this:
ToNumberturns a value into a number.ToPrimitiveturns an object into a primitive, like a string or a number.ToBooleanturns a value intotrueorfalse.
== uses the first two.
You want to know if a price in dollars and a price in rupees are the same. First you change the dollars into rupees. Then you compare. Loose equality does the same kind of work: it may convert values before it compares them.
- In real life: Both prices are already in rupees
- In JavaScript: Same type: compare with
=== - In real life: Both price labels are missing
- In JavaScript:
null == undefinedis true - In real life: Change one price into rupees first
- In JavaScript: Convert a string, boolean, or object first
- In real life: A price that can't be changed into rupees
- In JavaScript: No rule fits, so the answer is
false
Here are the steps of ==, in order:
- Same type? Compare with strict equality.
- One side is
nulland the other isundefined? The answer istrue. Next to anything else,nullandundefinedgivefalse. - A number and a string? Turn the string into a number with
ToNumber, then start again. - A boolean? Turn it into a number:
truebecomes1andfalsebecomes0. Then start again. - An object and a primitive? Turn the object into a primitive with
ToPrimitive, then start again. - A BigInt and a string or a number? Use the BigInt rules shown below.
- No rule fits? The answer is
false.
console.log(null == undefined);console.log(false == 0);console.log("" == 0);console.log("tea" == 0);- Line 1 prints
true. That is the specialnullandundefinedrule. - Line 2 prints
true.falsebecomes0, and0equals0. - Line 3 prints
true. The empty string""becomes0. - Line 4 prints
false. The word"tea"can't become a real number, so it becomesNaN. AndNaNis not equal to0.
A BigInt is a whole number of any size. You write it with an n at the end, like 0n. Loose equality has its own rules for BigInts.
console.log(0n == "0");console.log(0n === 0);console.log(0n == 0);console.log(1n == 1.5);- Line 1 prints
true.==reads the string"0"as the BigInt0n. - Line 2 prints
false.===never converts, and a BigInt and a number are different types. - Line 3 prints
true.==compares a BigInt and a number by their value. - Line 4 prints
false.1.5is not a whole number, so it can't match any BigInt.
The picture shows the steps for false == "". The step-through below starts with this same pair.
false becomes 0. Then the empty string "" becomes 0. Now both sides are numbers, so == compares them like === and gives true.Walk through the steps of == for the pair you chose. The last result is checked against real JavaScript ==.
script
// 1. Same type? Compare with ===.// 2. null and undefined match only each other.// 3. Strings, booleans, and objects may be converted.// 4. BigInts have their own string and number rules.// 5. No rule fits? The answer is false.value == null is a short way to write value === null || value === undefined. Many teams use it on purpose. It does not match false, 0, or the empty string "".
Object.is, NaN & -0
STEP THROUGHObject.is(a, b) is a function that compares two values. It uses the rules the spec calls SameValue. It never converts values. For objects, it checks the reference, just like ===. It gives a different answer from === in only two cases:
Object.is(NaN, NaN)istrue.===saysfalse.Object.is(-0, +0)isfalse.===saystrue.
=== and Object.is agree on almost every pair. These two pairs are the only ones where they give opposite answers.const score = Number("not a score"); console.log(Number.isNaN(score));console.log(Object.is(score, NaN));console.log(Object.is(-0, 0));- Line 1 tries to turn the text
"not a score"into a number. That fails, soscoreisNaN. - Line 3 prints
true.Number.isNaNis a function made for exactly this check. - Line 4 asks the same question with
Object.is. It also printstrue. - Line 5 prints
false, because SameValue keeps negative zero apart from positive zero.
Step through the two values that === treats in a surprising way: NaN and negative zero.
script
console.log(rounded);console.log(1 / rounded);console.log(Object.is(rounded, -0));console.log(JSON.parse(JSON.stringify(rounded)));console.log(Number.isNaN(NaN));console.log(NaN !== NaN);Where does negative zero come from? Some calculations make it, such as Math.round(-0.4) or -1 * 0. Code that only shows numbers on a page can usually ignore it. But some number code can't, because 1 / -0 gives -Infinity while 1 / 0 gives Infinity. JSON loses the sign: JSON.stringify(-0) gives the text "0".
Use Number.isNaN(value). You may also see the old trick value !== value. It works because NaN is the only value that is not equal to itself. But it is hard to read, so prefer Number.isNaN.
SameValueZero in includes & Map
INTERACTIVESameValueZero is the set of rules that lists and collections use to find values. It takes one rule from each of the other two:
- Like
Object.is, it saysNaNis the same asNaN. - Like
===, it says-0and+0are the same.
Three tools use it. includes checks whether an array holds a value. A Set is a collection that keeps each value only once. A Map stores values under keys. So [NaN].includes(NaN) is true, and new Set([-0]).has(0) is true. But [NaN].indexOf(NaN) is -1, because indexOf uses ===.
const tasks = [NaN, "tea", NaN]; console.log(tasks.includes(NaN));console.log(tasks.indexOf(NaN)); const ids = new Set([1, 1, 2]);console.log(ids.size);- Line 3 prints
true.includesfindsNaN. - Line 4 prints
-1, which means "not found".indexOfuses===, andNaN === NaNisfalse. - Line 6 puts
1,1, and2into aSet, which keeps each value once. Line 7 prints2.
includes uses SameValueZero, where NaN matches NaN, so it finds one. indexOf uses ===, where NaN never matches, so it gives -1: not found.See why includes, Map, and Set can find NaN and treat -0 and +0 as one value, while indexOf can't find NaN.
script
console.log([NaN].indexOf(NaN));const map = new Map([[NaN, "stored"]]);console.log(map.get(NaN));const set = new Set([-0]);console.log(set.has(0));console.log([NaN].includes(NaN));console.log([NaN].indexOf(NaN));const map = new Map([[NaN, "stored"]]);console.log(map.get(NaN));const set = new Set([-0]);console.log(set.has(0));new Map([[NaN, "stored"]]).get(NaN) returns stored.
Map keys use SameValueZero, so you can get a value stored under NaN by asking with another NaN.
The table below asks all four sets of rules about four well-known pairs. Read each row as: "for this pair, each set of rules gives this answer."
| Pair | === | == | Object.is | SameValueZero |
|---|---|---|---|---|
NaN / NaN | false | false | true | true |
-0 / +0 | true | true | false | true |
"1" / 1 | false | true | false | false |
null / undefined | false | true | false | false |
Where you'll use this
Real code uses a few simple choices:
- Use
===for everyday comparisons. - Use
value == nullwhen you mean "the value is missing", whether it isnullorundefined. - Use
Object.iswhen your code must treatNaNas equal toNaN, or must see the sign of zero.
React, a popular library for building pages, uses Object.is to check whether some state has changed. So these details sometimes matter when you fix bugs in a page.
if (user.middleName == null) { showOptionalNameField();} if (Object.is(nextTemperature, previousTemperature)) { skipChartUpdate();} const uniqueIds = new Set(idsFromSeveralLists);- Line 1 checks for a missing middle name:
nullorundefined. - Line 5 skips redrawing a chart when the temperature has not changed.
- Line 9 makes a
Set, which keeps each ID only once.
const profile = { nickname: null };const ageText = "3";const ids = [1, 1, 2]; console.log(profile.nickname == null);console.log(Number(ageText) === 3);console.log(new Set(ids).size);- Line 5 checks a profile field that may be missing in either way. It prints
true. - Line 6 turns the form text into a number first, then uses
===. It printstrue. - Line 7 drops the repeated ID and prints
2.
const savedPin = 1234;const enteredText = "1234";const enteredPin = Number(enteredText); console.log(enteredPin === savedPin);Line 2 is text, because form fields always give you strings. Line 3 turns it into a number on purpose. Now line 5 can use ===, and no hidden conversion can let a wrong value through. Real passwords are different: don't compare them in page code at all. Let the server check them.
A Set does some of this work for you. new Set(values) uses SameValueZero to remove repeats. So all the NaN values become one, and -0 and +0 count as one value.
Common misconceptions
“Strict equality works like equality in maths.”
It does for normal numbers. But NaN === NaN is false, and -0 === +0 is true. These follow JavaScript's number rules, not school maths.
“Loose equality is random.”
It follows fixed steps in a fixed order. But the steps are easy to misread. Follow them one by one, or use == only for clear patterns like value == null.
“Objects that look the same are equal.”
Two objects can have the same properties and still be two separate objects. === compares references. It never compares the properties inside.
“Object.is is just a stricter ===.”
It is stricter about zero: it says -0 and +0 are different. But it is less strict about NaN: it says NaN is the same as NaN. It is different, not just stricter.
“includes and indexOf search the same way.”
They differ for NaN. includes uses SameValueZero, so it finds NaN. indexOf uses ===, so it doesn't.
- Turns "1" into 1, so "1" and 1 match
- Says null and undefined match
- Says NaN matches NaN, but -0 does not match +0
[NaN].includes(NaN)is true- Never converts, and says -0 matches +0
new Set([-0]).has(0)is trueobj === objis trueObject.is(-0, +0)is false
Each card is a true statement. Put it under the set of rules that it describes.
Practice: equality & sameness
5 EXERCISESPredict the three printed values, in order. Each one is true or false.
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));
console.log([NaN].includes(NaN));The output is false true true. Strict equality says NaN is not equal to itself. Object.is and SameValueZero both say NaN matches NaN.
"0" == falseWrite the printed value. Then say each step to yourself: what changes, and into what?
console.log("0" == false);false becomes 0, so the question starts again as "0" == 0. Now it is a string and a number, so "0" becomes 0. Then 0 === 0 is true.
Change the function body so it returns true for a real NaN, and false for the string "NaN".
function isActuallyNaN(value) {
return value === NaN;
}function isActuallyNaN(value) {
return Number.isNaN(value);
}
console.log(isActuallyNaN(NaN));
console.log(isActuallyNaN("NaN"));Number.isNaN(NaN) is true and Number.isNaN("NaN") is false. That is the check you usually want.
Predict the first printed value. Then explain why the list of unique values still has NaN in it.
const values = [NaN, NaN, -0, 0, "0", "0"];
const unique = [...new Set(values)];
console.log(unique.length);
console.log(unique.includes(NaN));The list of unique values has three items: one NaN, one zero, and one string "0". SameValueZero is why the two NaN values become one.
In one sentence, explain why Object.is(-0, 0) is false even though -0 === 0 is true.
console.log(Object.is(-0, 0));
console.log(1 / -0);Object.is(-0, 0) is false because SameValue keeps the sign of zero. You can see that sign with 1 / -0, which gives -Infinity.
Quiz: check your understanding
8 QUESTIONSFor each question, first name the set of rules. Then follow its steps. Every answer comes with an explanation.
Question 1 of 8Which check should you use by default in everyday code?
Choose an answer to see the explanation.
Question 2 of 8What does
"1" == 1print?Read the code, then predictconsole.log("1" == 1);Choose an answer to see the explanation.
Question 3 of 8What does
NaN === NaNprint?Read the code, then predictconsole.log(NaN === NaN);Choose an answer to see the explanation.
Question 4 of 8What does
Object.is(-0, 0)print?Read the code, then predictconsole.log(Object.is(-0, 0));Choose an answer to see the explanation.
Question 5 of 8Which one uses SameValueZero?
Choose an answer to see the explanation.
Question 6 of 8What does
null == undefinedprint?Read the code, then predictconsole.log(null == undefined);Choose an answer to see the explanation.
Question 7 of 8What does
[NaN].indexOf(NaN)print?Read the code, then predictconsole.log([NaN].indexOf(NaN));Choose an answer to see the explanation.
Question 8 of 8Which line is a good, on-purpose use of loose equality?
Choose an answer to see the explanation.
Key takeaways
- Use
===by default. It checks for the same type and the same value, and never converts. ==follows fixed conversion steps in order. Use it on purpose, mostly asvalue == null.Object.isuses SameValue.NaNmatchesNaN, and-0is different from+0.includes,Map, andSetuse SameValueZero.NaNmatchesNaN, and-0matches+0.- When both sides are objects, all four compare references. None of them compares the properties inside.
In one line:
JavaScript has four ways to check sameness: === never converts, == may convert first, Object.is notices NaN and the sign of zero, and SameValueZero is what lists and collections use.
Up next: Checking types reliably.