Operator Precedence

As we covered in Chapter 4, JavaScript’s version of && and || are interesting in that they select and return one of their operands, rather than just resulting in true or false. That’s easy to reason about if there are only two operands and one operator.

  1. var a = 42;
  2. var b = "foo";
  3. a && b; // "foo"
  4. a || b; // 42

But what about when there’s two operators involved, and three operands?

  1. var a = 42;
  2. var b = "foo";
  3. var c = [1,2,3];
  4. a && b || c; // ???
  5. a || b && c; // ???

To understand what those expressions result in, we’re going to need to understand what rules govern how the operators are processed when there’s more than one present in an expression.

These rules are called “operator precedence.”

I bet most readers feel they have a decent grasp on operator precedence. But as with everything else we’ve covered in this book series, we’re going to poke and prod at that understanding to see just how solid it really is, and hopefully learn a few new things along the way.

Recall the example from above:

  1. var a = 42, b;
  2. b = ( a++, a );
  3. a; // 43
  4. b; // 43

But what would happen if we remove the ( )?

  1. var a = 42, b;
  2. b = a++, a;
  3. a; // 43
  4. b; // 42

Wait! Why did that change the value assigned to b?

Because the , operator has a lower precedence than the = operator. So, b = a++, a is interpreted as (b = a++), a. Because (as we explained earlier) a++ has after side effects, the assigned value to b is the value 42 before the ++ changes a.

This is just a simple matter of needing to understand operator precedence. If you’re going to use , as a statement-series operator, it’s important to know that it actually has the lowest precedence. Every other operator will more tightly bind than , will.

Now, recall this example from above:

  1. if (str && (matches = str.match( /[aeiou]/g ))) {
  2. // ..
  3. }

We said the ( ) around the assignment is required, but why? Because && has higher precedence than =, so without the ( ) to force the binding, the expression would instead be treated as (str && matches) = str.match... But this would be an error, because the result of (str && matches) isn’t going to be a variable, but instead a value (in this case undefined), and so it can’t be the left-hand side of an = assignment!

OK, so you probably think you’ve got this operator precedence thing down.

Let’s move on to a more complex example (which we’ll carry throughout the next several sections of this chapter) to really test your understanding:

  1. var a = 42;
  2. var b = "foo";
  3. var c = false;
  4. var d = a && b || c ? c || b ? a : c && b : a;
  5. d; // ??

OK, evil, I admit it. No one would write a string of expressions like that, right? Probably not, but we’re going to use it to examine various issues around chaining multiple operators together, which is a very common task.

The result above is 42. But that’s not nearly as interesting as how we can figure out that answer without just plugging it into a JS program to let JavaScript sort it out.

Let’s dig in.

The first question — it may not have even occurred to you to ask — is, does the first part (a && b || c) behave like (a && b) || c or like a && (b || c)? Do you know for certain? Can you even convince yourself they are actually different?

  1. (false && true) || true; // true
  2. false && (true || true); // false

So, there’s proof they’re different. But still, how does false && true || true behave? The answer:

  1. false && true || true; // true
  2. (false && true) || true; // true

So we have our answer. The && operator is evaluated first and the || operator is evaluated second.

But is that just because of left-to-right processing? Let’s reverse the order of operators:

  1. true || false && false; // true
  2. (true || false) && false; // false -- nope
  3. true || (false && false); // true -- winner, winner!

Now we’ve proved that && is evaluated first and then ||, and in this case that was actually counter to generally expected left-to-right processing.

So what caused the behavior? Operator precedence.

Every language defines its own operator precedence list. It’s dismaying, though, just how uncommon it is that JS developers have read JS’s list.

If you knew it well, the above examples wouldn’t have tripped you up in the slightest, because you’d already know that && is more precedent than ||. But I bet a fair amount of readers had to think about it a little bit.

Note: Unfortunately, the JS spec doesn’t really have its operator precedence list in a convenient, single location. You have to parse through and understand all the grammar rules. So we’ll try to lay out the more common and useful bits here in a more convenient format. For a complete list of operator precedence, see “Operator Precedence” on the MDN site (* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence).

Short Circuited

In Chapter 4, we mentioned in a side note the “short circuiting” nature of operators like && and ||. Let’s revisit that in more detail now.

For both && and || operators, the right-hand operand will not be evaluated if the left-hand operand is sufficient to determine the outcome of the operation. Hence, the name “short circuited” (in that if possible, it will take an early shortcut out).

For example, with a && b, b is not evaluated if a is falsy, because the result of the && operand is already certain, so there’s no point in bothering to check b. Likewise, with a || b, if a is truthy, the result of the operand is already certain, so there’s no reason to check b.

This short circuiting can be very helpful and is commonly used:

  1. function doSomething(opts) {
  2. if (opts && opts.cool) {
  3. // ..
  4. }
  5. }

The opts part of the opts && opts.cool test acts as sort of a guard, because if opts is unset (or is not an object), the expression opts.cool would throw an error. The opts test failing plus the short circuiting means that opts.cool won’t even be evaluated, thus no error!

Similarly, you can use || short circuiting:

  1. function doSomething(opts) {
  2. if (opts.cache || primeCache()) {
  3. // ..
  4. }
  5. }

Here, we’re checking for opts.cache first, and if it’s present, we don’t call the primeCache() function, thus avoiding potentially unnecessary work.

Tighter Binding

But let’s turn our attention back to that earlier complex statement example with all the chained operators, specifically the ? : ternary operator parts. Does the ? : operator have more or less precedence than the && and || operators?

  1. a && b || c ? c || b ? a : c && b : a

Is that more like this:

  1. a && b || (c ? c || (b ? a : c) && b : a)

or this?

  1. (a && b || c) ? (c || b) ? a : (c && b) : a

The answer is the second one. But why?

Because && is more precedent than ||, and || is more precedent than ? :.

So, the expression (a && b || c) is evaluated first before the ? : it participates in. Another way this is commonly explained is that && and || “bind more tightly” than ? :. If the reverse was true, then c ? c... would bind more tightly, and it would behave (as the first choice) like a && b || (c ? c..).

Associativity

So, the && and || operators bind first, then the ? : operator. But what about multiple operators of the same precedence? Do they always process left-to-right or right-to-left?

In general, operators are either left-associative or right-associative, referring to whether grouping happens from the left or from the right.

It’s important to note that associativity is not the same thing as left-to-right or right-to-left processing.

But why does it matter whether processing is left-to-right or right-to-left? Because expressions can have side effects, like for instance with function calls:

  1. var a = foo() && bar();

Here, foo() is evaluated first, and then possibly bar() depending on the result of the foo() expression. That definitely could result in different program behavior than if bar() was called before foo().

But this behavior is just left-to-right processing (the default behavior in JavaScript!) — it has nothing to do with the associativity of &&. In that example, since there’s only one && and thus no relevant grouping here, associativity doesn’t even come into play.

But with an expression like a && b && c, grouping will happen implicitly, meaning that either a && b or b && c will be evaluated first.

Technically, a && b && c will be handled as (a && b) && c, because && is left-associative (so is ||, by the way). However, the right-associative alternative a && (b && c) behaves observably the same way. For the same values, the same expressions are evaluated in the same order.

Note: If hypothetically && was right-associative, it would be processed the same as if you manually used ( ) to create grouping like a && (b && c). But that still doesn’t mean that c would be processed before b. Right-associativity does not mean right-to-left evaluation, it means right-to-left grouping. Either way, regardless of the grouping/associativity, the strict ordering of evaluation will be a, then b, then c (aka left-to-right).

So it doesn’t really matter that much that && and || are left-associative, other than to be accurate in how we discuss their definitions.

But that’s not always the case. Some operators would behave very differently depending on left-associativity vs. right-associativity.

Consider the ? : (“ternary” or “conditional”) operator:

  1. a ? b : c ? d : e;

? : is right-associative, so which grouping represents how it will be processed?

  • a ? b : (c ? d : e)
  • (a ? b : c) ? d : e

The answer is a ? b : (c ? d : e). Unlike with && and || above, the right-associativity here actually matters, as (a ? b : c) ? d : e will behave differently for some (but not all!) combinations of values.

One such example:

  1. true ? false : true ? true : true; // false
  2. true ? false : (true ? true : true); // false
  3. (true ? false : true) ? true : true; // true

Even more nuanced differences lurk with other value combinations, even if the end result is the same. Consider:

  1. true ? false : true ? true : false; // false
  2. true ? false : (true ? true : false); // false
  3. (true ? false : true) ? true : false; // false

From that scenario, the same end result implies that the grouping is moot. However:

  1. var a = true, b = false, c = true, d = true, e = false;
  2. a ? b : (c ? d : e); // false, evaluates only `a` and `b`
  3. (a ? b : c) ? d : e; // false, evaluates `a`, `b` AND `e`

So, we’ve clearly proved that ? : is right-associative, and that it actually matters with respect to how the operator behaves if chained with itself.

Another example of right-associativity (grouping) is the = operator. Recall the chained assignment example from earlier in the chapter:

  1. var a, b, c;
  2. a = b = c = 42;

We asserted earlier that a = b = c = 42 is processed by first evaluating the c = 42 assignment, then b = .., and finally a = ... Why? Because of the right-associativity, which actually treats the statement like this: a = (b = (c = 42)).

Remember our running complex assignment expression example from earlier in the chapter?

  1. var a = 42;
  2. var b = "foo";
  3. var c = false;
  4. var d = a && b || c ? c || b ? a : c && b : a;
  5. d; // 42

Armed with our knowledge of precedence and associativity, we should now be able to break down the code into its grouping behavior like this:

  1. ((a && b) || c) ? ((c || b) ? a : (c && b)) : a

Or, to present it indented if that’s easier to understand:

  1. (
  2. (a && b)
  3. ||
  4. c
  5. )
  6. ?
  7. (
  8. (c || b)
  9. ?
  10. a
  11. :
  12. (c && b)
  13. )
  14. :
  15. a

Let’s solve it now:

  1. (a && b) is "foo".
  2. "foo" || c is "foo".
  3. For the first ? test, "foo" is truthy.
  4. (c || b) is "foo".
  5. For the second ? test, "foo" is truthy.
  6. a is 42.

That’s it, we’re done! The answer is 42, just as we saw earlier. That actually wasn’t so hard, was it?

Disambiguation

You should now have a much better grasp on operator precedence (and associativity) and feel much more comfortable understanding how code with multiple chained operators will behave.

But an important question remains: should we all write code understanding and perfectly relying on all the rules of operator precedence/associativity? Should we only use ( ) manual grouping when it’s necessary to force a different processing binding/order?

Or, on the other hand, should we recognize that even though such rules are in fact learnable, there’s enough gotchas to warrant ignoring automatic precedence/associativity? If so, should we thus always use ( ) manual grouping and remove all reliance on these automatic behaviors?

This debate is highly subjective, and heavily symmetrical to the debate in Chapter 4 over implicit coercion. Most developers feel the same way about both debates: either they accept both behaviors and code expecting them, or they discard both behaviors and stick to manual/explicit idioms.

Of course, I cannot answer this question definitively for the reader here anymore than I could in Chapter 4. But I’ve presented you the pros and cons, and hopefully encouraged enough deeper understanding that you can make informed rather than hype-driven decisions.

In my opinion, there’s an important middle ground. We should mix both operator precedence/associativity and ( ) manual grouping into our programs — I argue the same way in Chapter 4 for healthy/safe usage of implicit coercion, but certainly don’t endorse it exclusively without bounds.

For example, if (a && b && c) .. is perfectly OK to me, and I wouldn’t do if ((a && b) && c) .. just to explicitly call out the associativity, because I think it’s overly verbose.

On the other hand, if I needed to chain two ? : conditional operators together, I’d certainly use ( ) manual grouping to make it absolutely clear what my intended logic is.

Thus, my advice here is similar to that of Chapter 4: use operator precedence/associativity where it leads to shorter and cleaner code, but use ( ) manual grouping in places where it helps create clarity and reduce confusion.