JavaScript has two ways to ask “are these equal?”
===is strict equality. It checks the value and the type. No conversion.==is loose equality. If the types differ, JavaScript converts them first, then compares.
console.log(5 === 5); // → true
console.log(5 === "5"); // → false (number vs string)
console.log(5 == "5"); // → true (the string is converted)
Why loose equality causes trouble
The conversion rules are complicated, and some results are unexpected.
console.log(0 == ""); // → true
console.log(0 == "0"); // → true
console.log("" == "0"); // → false
console.log(false == "0"); // → true
console.log(null == undefined); // → true
console.log(null == 0); // → false
Look at 0 == "" and "" == "0": the first is true, the second false, although 0 == "0" is true. Equality that is not transitive is hard to reason about.
The rule
Use === and !== by default. You always know what they do. The only common reason to use == is the one-liner value == null, which is true for both null and undefined, and many teams prefer to write that explicitly (or use ?? and ?.).
Objects are compared by reference
Both operators compare objects and arrays by identity, not by content.
const a = { x: 1 };
const b = { x: 1 };
const c = a;
console.log(a === b); // → false (two different objects)
console.log(a === c); // → true (same object)
To compare contents you have to compare the properties yourself, for example with JSON.stringify for simple data, or a helper function.
Object.is
Object.is(a, b) is a rare third option. It works like ===, except that Object.is(NaN, NaN) is true and Object.is(0, -0) is false. You will not need it daily.
Make it a habit
eqeqeq flags every accidental ==, which is a good way to learn.