Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 3

Operators, Coercion, and Equality

This chapter assumes you are comfortable with values and types in JavaScript (primitives vs. objects) and with variable declaration. You should also have written and run JavaScript before, in a browser console or Node.
Level: AdvancedLength: In depth4 min read

Read at another depth

Operators, Coercion, and Equality

This chapter assumes you are comfortable with values and types in JavaScript (primitives vs. objects) and with variable declaration. You should also have written and run JavaScript before, in a browser console or Node.

JavaScript's operator semantics are where the language's reputation for surprise lives. The surprises are not random: they follow from a small set of rules — the abstract operations the spec runs before every comparison, arithmetic expression, or concatenation. Once you know those operations, [] + {} stops being a joke and becomes a prediction.

The Abstract Operations

Three internal operations do almost all the work:

  • ToPrimitive(value, hint) — converts a value to a primitive before an operator uses it. It calls [Symbol.toPrimitive](hint), then valueOf(), then toString(), stopping at the first one that returns a primitive. The hint is "default", "number", or "string".
  • ToNumber(value) — used by arithmetic, + with a numeric hint, and relational operators.
  • ToString(value) — used when the string hint wins.

The critical asymmetry: ToPrimitive for the + operator uses the "default" hint, while relational operators (<, >) use the "number" hint. That difference explains several famous results.

Code example in javascript
const a = [1, 2];
const b = ['x'];

// default hint on arrays: valueOf returns the array itself (not a primitive),
// so toString is used -> "1,2"
console.log(a + b);            // "1,2x"
console.log(a + 1);            // "1,21"  ("1,2" + "1")

// numeric hint: array -> null -> 0
console.log([] == 0);          // true
console.log([5] - 1);          // 4

// {} in expression position is a block, so this is unary + applied to []
console.log(+[]);              // 0
console.log([] + {});          // "[object Object]"
console.log({} + []);          // "[object Object]" only as a statement... 

Note the last caveat: pasted into a REPL as a statement, {} + [] yields 0 because {} parses as an empty block. In an expression context (console.log({} + [])) it is "[object Object]". Parser context, not coercion, produces this result.

You can control the conversion path per-type by defining Symbol.toPrimitive:

Code example in javascript
class Money {
  constructor(cents) { this.cents = cents; }
  [Symbol.toPrimitive](hint) {
    return hint === 'number' ? this.cents / 100 : `$${(this.cents / 100).toFixed(2)}`;
  }
}
const price = new Money(1999);
console.log(price + 0);        // 19.99
console.log(`${price}`);       // $19.99

Prefer this over overriding valueOf/toString when the numeric and string views of a type genuinely differ; it removes the ordering ambiguity between the two methods.

`==` vs. `===`: The Actual Algorithm

=== (strict equality) compares values without conversion — except one historical wrinkle: NaN !== NaN, and +0 === -0 is true. (Use Object.is when you want NaN to equal itself and +0/-0 to differ; it matters for Map keys and sign-aware code.)

== (loose equality) runs a defined algorithm:

  1. If both operands share a type, it behaves like === (with null == undefined as a special pair).
  2. null/undefined equal each other and nothing else: null == 0 is false, famously.
  3. Number vs. string: the string is converted to a number.
  4. Boolean operand: the boolean is converted to a number first — a classic trap, since true == '1' becomes 1 == 1.
  5. Primitive vs. object: the object is converted with ToPrimitive (default hint).
  6. Otherwise: false.

Two consequences worth memorizing:

Code example in javascript
console.log('0' == false);   // true  (false -> 0, '0' -> 0)
console.log('' == false);    // true
console.log('' == '0');      // false (same types, string comparison)
console.log(null <= 0);      // true  (< and == use different algorithms!)
console.log(null == 0);      // false

That last pair is the deepest gotcha: relational operators coerce null to 0, but == gives null special treatment, so x <= null and x == null disagree.

Decision rule. Use === by default. Use == deliberately and only in two idiomatic places: x == null (catches both null and undefined) and, rarely, comparing against a loosely-typed value from a legacy API. Lint tools such as ESLint's eqeqeq with the smart option encode exactly this policy.

Falsy Values and Their Edge Cases

Exactly eight values are falsy: false, 0, -0, 0n (BigInt zero), "", null, undefined, NaN. Everything else — including [], {}, new Boolean(false), and new Number(0) — is truthy, because all objects are truthy regardless of their wrapped value. That is why "is this array empty?" must be arr.length === 0, never !arr.

For empty-string checks, if (str) silently conflates undefined, null, and "". If those cases differ, test explicitly.

Operator Precedence and Associativity Traps

Coercion bugs often hide behind precedence:

Code example in javascript
console.log(1 + 2 + '3');     // "33"  (left to right: 3 + '3')
console.log('1' + 2 + 3);     // "123"
console.log(1 + true);        // 2
console.log(NaN || 'x');      // 'x'   — NaN is falsy
console.log(0 ?? 'fallback'); // 0     — nullish coalescing ignores falsiness
console.log('' ?? 'fallback'); // ''

|| and && fall back on falsiness; ?? falls back only on null/undefined. Reach for ?? whenever 0 or "" are legitimate values. Also note ?? cannot be mixed with &&/|| without parentheses — the spec forbids the ambiguity.

Short-circuiting is itself the point of && and || in expressions; but remember they return an operand, not a boolean: a && b yields a when a is falsy. Code like const enabled = flag === true may be redundant; const enabled = Boolean(flag) is the explicit, coercion-safe form.

A Mental Model and a Defensive Toolkit

Code example in javascript
// Predict, then verify:
console.log(1 < 2 < 3);      // true   -> (1 < 2) is true -> 1 < 3
console.log(3 > 2 > 1);      // false! -> (3 > 2) is true -> 1 > 1 is false
console.log([10.9] == 10);   // false  -> "10.9" -> 10.9

// Tools that sidestep coercion:
Number('42');        // 42; Number('') is 0, Number('x') is NaN
Number('');          // 0  — trap
parseInt('', 10);    // NaN — safer for user input
Object.is(NaN, NaN); // true
Object.is(0, -0);    // false

Sanitize external input with Number.isFinite(Number(x)) rather than truthiness checks, and let types (TypeScript, JSDoc) shrink the surface where coercion can bite at all.

Code example in {callout tip}
When debugging a coercion surprise, evaluate each operand's `String(x)` and `Number(x)` separately first. Most `==` mysteries resolve once you see what each side becomes before comparison.

Exercise

What does [] == ![] evaluate to, and why?

Show answer

true. ![] converts the (truthy) array to false, then to the number 0. The left side [] is converted via ToPrimitive to the string "", then to the number 0. So 0 == 0.

Exercise

Why is null >= 0 true while null == 0 is false?

Show answer

Relational operators (>=) convert null to the number 0 via ToNumber, so 0 >= 0 holds. Loose equality gives null special treatment: it equals only undefined, never any other value, and no conversion is attempted.

Exercise

You need a fallback for a numeric setting that may be legitimately 0. Which operator do you use and why?

Show answer

?? (nullish coalescing). It falls back only when the value is null or undefined, preserving 0. Using || would replace 0 with the fallback because 0 is falsy.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Operators, Coercion, and Equality

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that