HomepageExploring JavaScript (ES2025 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

15 Operators

15.1 Making sense of operators

JavaScript’s operators sometimes produce unintuitive results. With the following two rules, they are easier to understand:

15.1.1 Operators coerce their operands to appropriate types

If an operator gets operands that don’t have the proper types, it rarely throws an exception. Instead, it coerces (automatically converts) the operands so that it can work with them. Let’s look at two examples.

First, the multiplication operator can only work with numbers. Therefore, it converts strings to numbers before computing its result.

> '7' * '3'
21

Second, the square brackets operator ([ ]) for accessing the properties of an object can only handle strings and symbols. All other values are coerced to string:

const obj = {};
obj['true'] = 123;

// Coerce true to the string 'true'
assert.equal(obj[true], 123);

15.1.2 Most operators only work with primitive values

As mentioned before, most operators only work with primitive values. If an operand is an object, it is usually coerced to a primitive value – for example:

> [1,2,3] + [4,5,6]
'1,2,34,5,6'

Why? The plus operator first coerces its operands to primitive values:

> String([1,2,3])
'1,2,3'
> String([4,5,6])
'4,5,6'

Next, it concatenates the two strings:

> '1,2,3' + '4,5,6'
'1,2,34,5,6'

15.2 Converting values to primitives (advanced)

The following JavaScript code explains how arbitrary values are converted to primitive values:

import assert from 'node:assert/strict';

/**
 * @param {any} input 
 * @param {'STRING'|'NUMBER'} [preferredType] optional
 * @returns {primitive}
 * @see https://tc39.es/ecma262/#sec-toprimitive
 */
function ToPrimitive(input, preferredType) {
  if (isObject(input)) {
    // `input` is an object
    const exoticToPrim = input[Symbol.toPrimitive]; // (A)
    if (exoticToPrim !== undefined) {
      let hint;
      if (preferredType === undefined) {
        hint = 'default';
      } else if (preferredType === 'STRING') {
        hint = 'string';
      } else {
        assert(preferredType === 'NUMBER');
        hint = 'number';
      }
      const result = exoticToPrim.apply(input, [hint]);
      if (!isObject(result)) return result;
      throw new TypeError();
    }
    if (preferredType === undefined) {
      preferredType = 'NUMBER';
    }
    return OrdinaryToPrimitive(input, preferredType);
  }
  // `input` is primitive
  return input;
}

/**
 * @param {object} O 
 * @param {'STRING'|'NUMBER'} hint 
 * @returns {primitive}
 */
function OrdinaryToPrimitive(O, hint) {
  let methodNames;
  if (hint === 'STRING') {
    methodNames = ['toString', 'valueOf'];
  } else {
    methodNames = ['valueOf', 'toString'];
  }
  for (const name of methodNames) {
    const method = O[name];
    if (isCallable(method)) {
      const result = method.apply(O);
      if (!isObject(result)) return result;
    }
  }
  throw new TypeError();
}

function isObject(value) {
  return typeof value === 'object' && value !== null;
}

function isCallable(value) {
  return typeof value === 'function';
}

Only the following objects define a method with the key Symbol.toPrimitive:

Therefore, let’s focus on OrdinaryToPrimitive(): If we prefer strings, .toString() is called first. If we prefer numbers, .valueOf() is called first. We can see that in the following code.

const obj = {
  toString() {
    return '1';
  },
  valueOf() {
    return 2;
  },
};
assert.equal(
  String(obj), '1'
);
assert.equal(
  Number(obj), 2
);

15.3 The plus operator (+)

The plus operator works as follows in JavaScript:

String mode lets us use + to assemble strings:

> 'There are ' + 3 + ' items'
'There are 3 items'

Number mode means that if neither operand is a string (or an object that becomes a string) then everything is coerced to numbers:

> 4 + true
5

Number(true) is 1.

15.4 Assignment operators

15.4.1 The plain assignment operator

The plain assignment operator is used to change storage locations:

x = value; // assign to a previously declared variable
obj.propKey = value; // assign to a property
arr[index] = value; // assign to an Array element

Initializers in variable declarations can also be viewed as a form of assignment:

const x = value;
let y = value;

15.4.2 Compound assignment operators

JavaScript supports the following assignment operators:

15.4.2.1 Logical assignment operators ES2021

Logical assignment operators work differently from other compound assignment operators:

Assignment operatorEquivalent toOnly assigns if a is
a ||= ba || (a = b)Falsy
a &&= ba && (a = b)Truthy
a ??= ba ?? (a = b)Nullish

Why is a ||= b equivalent to the following expression?

a || (a = b)

Why not to this expression?

a = a || b

The former expression has the benefit of short-circuiting: The assignment is only evaluated if a evaluates to false. Therefore, the assignment is only performed if it’s necessary. In contrast, the latter expression always performs an assignment.

For more on ??=, see “The nullish coalescing assignment operator (??=) ES2021” (§16.4.4).

15.4.2.2 The remaining compound assignment operators

For operators op other than || && ??, the following two ways of assigning are equivalent:

myvar op= value
myvar = myvar op value

If, for example, op is +, then we get the operator += that works as follows.

let str = '';
str += '<b>';
str += 'Hello!';
str += '</b>';

assert.equal(str, '<b>Hello!</b>');

15.5 Equality: == vs. === vs. Object.is()

JavaScript has two kinds of equality operators:

Icon “tip”Recommendation: always use strict equality (===)

Loose equality has many quirks and is difficult to understand. My recommendation is to always use strict equality. I’ll explain how loose equality works but it’s not something worth remembering.

15.5.1 Strict equality (=== and !==)

Two values are only strictly equal if they have the same type. Strict equality never coerces.

Primitive values (including strings and excluding symbols) are compared by value:

> undefined === null
false
> null === null
true

> true === false
false
> true === true
true

> 1 === 2
false
> 3 === 3
true

> 'a' === 'b'
false
> 'c' === 'c'
true

All other values must have the same identity:

> {} === {} // two different empty objects
false

> const obj = {};
> obj === obj
true

Symbols are compared similarly to objects:

> Symbol() === Symbol() // two different symbols
false
> const sym = Symbol();
> sym === sym
true

The number error value NaN is famously not strictly equal to itself (because, internally, it’s not a single value):

> typeof NaN
'number'
> NaN === NaN
false

15.5.2 Loose equality (== and !=)

Loose equality is one of JavaScript’s quirks. Let’s explore its behavior.

15.5.2.1 If both operands have the same type

If both operands have the same primitive type, loose equality behaves like strict equality:

> 1 == 2
false
> 3 == 3
true
> 'a' == 'b'
false
> 'c' == 'c'
true

If both operands are objects, the same rule applies: Loose equality behaves like strict equality and they are only equal if they have the same identity.

> [1, 2, 3] == [1, 2, 3] // two different objects
false

> const arr = [1, 2, 3];
> arr == arr
true

Comparing symbols works similarly.

15.5.2.2 Coercion

If the operands have different types, loose equality often coerces. Some of those type coercions make sense:

> '123' == 123
true
> false == 0
true

Others less so:

> 0 == '\r\n\t ' // only whitespace
true

An object is coerced to a primitive value (only) if the other operand is primitive:

> [1, 2, 3] == '1,2,3'
true
> ['17'] == 17
true
15.5.2.3 == vs. Boolean()

Comparison with booleans is different from converting to boolean via Boolean():

> Boolean(0)
false
> Boolean(2)
true

> 0 == false
true
> 2 == true
false
> 2 == false
false

> Boolean('')
false
> Boolean('abc')
true

> '' == false
true
> 'abc' == true
false
> 'abc' == false
false
15.5.2.4 undefined == null

== considers undefined and null to be equal:

> undefined == null
true
15.5.2.5 How exactly does loose equality work? (advanced)

In the ECMAScript specification, loose equality is defined via the following operation:

IsLooselyEqual(x: any, y: any): boolean

As you can see, this algorithm is not exactly intuitive. Hence the following recommendation.

15.5.3 Recommendation: always use strict equality

I recommend to always use ===. It makes our code easier to understand and spares us from having to think about the quirks of ==.

Let’s look at two use cases for == and what I recommend to do instead.

15.5.3.1 Use case for ==: comparing with a number or a string

== lets us check if a value x is a number or that number as a string – with a single comparison:

if (x == 123) {
  // x is either 123 or '123'
}

I prefer either of the following two alternatives:

if (x === 123 || x === '123') ···
if (Number(x) === 123) ···

We can also convert x to a number when we first encounter it.

15.5.3.2 Use case for ==: comparing with undefined or null

Another use case for == is to check if a value x is either undefined or null:

if (x == null) {
  // x is either null or undefined
}

The problem with this code is that we can’t be sure if someone meant to write it that way or if they made a typo and meant === null.

I prefer this alternative:

if (x === undefined || x === null) ···

15.5.4 Even stricter than ===: Object.is() (advanced)

Method Object.is() compares two values:

> Object.is(3, 3)
true
> Object.is(3, 4)
false
> Object.is(3, '3')
false

Object.is() is even stricter than === – e.g.:

15.5.4.1 Detecting NaN via Object.is()

Object.is() considering NaN to be equal to itself is occasionally useful. For example, we can use it to implement an improved version of the Array method .indexOf():

const myIndexOf = (arr, elem) => {
  return arr.findIndex(x => Object.is(x, elem));
};

myIndexOf() finds NaN in an Array, while .indexOf() doesn’t:

> myIndexOf([0,NaN,2], NaN)
1
> [0,NaN,2].indexOf(NaN)
-1

The result -1 means that .indexOf() couldn’t find its argument in the Array.

15.6 Ordering operators

Operatorname
<less than
<=Less than or equal
>Greater than
>=Greater than or equal

Table 15.1: JavaScript’s ordering operators.

JavaScript’s ordering operators (table 15.1) work for both numbers and strings:

> 5 >= 2
true
> 'bar' < 'foo'
true

<= and >= are based on strict equality.

Icon “warning”The ordering operators don’t work well for human languages

The ordering operators don’t work well for comparing text in a human language, e.g., when capitalization or accents are involved. The details are explained in “Comparing strings” (§22.6).

15.7 Various other operators

The following operators are covered elsewhere in this book:

The next two subsections discuss two operators that are rarely used.

15.7.1 Comma operator

The comma operator has two operands, evaluates both of them and returns the second one:

const result = (console.log('evaluated'), 'YES');
assert.equal(
  result, 'YES'
);

Output:

evaluated

For more information on this operator, see Speaking JavaScript.

15.7.2 void operator

The void operator evaluates its operand and returns undefined:

const result = void console.log('evaluated');
assert.equal(
  result, undefined
);

Output:

evaluated

For more information on this operator, see Speaking JavaScript.