Understanding Operator Precedence in JavaScript
Introduction
I once had an embarrassing moment while trying to extract the current year. I wrote code like this:
new Date().getFullYear()
At that moment, I felt uncertain because I wasn't sure whether the . operator or the new keyword executes first. I quickly tested it in the console and—phew—no error occurred. The new operation runs first in this case.
Let's try something more adventurous by removing the parentheses after Date:
new Date.getFullYear()
This time, the browser threw an error. Why?
Or perhaps you've encountered baffling code snippets like this:
[[[] == []] + []][+![]][+![]] // Returns 'f'
If expressions like this leave you scratching your head, it's time to learn about operator precedence.
Understanding Expressions
According to MDN, operator precedence determines the order in which operations are evaluated in an expression. This concept exists specifically because expressions need a defined evaluation order. Before diving into precedence, let's establish what an expression actually is.
A practical (though not entirely rigorous) definition that resonates with many developers:
An expression is any piece of code that produces a value and can be placed wherever a value is expected.
You can test whether something qualifies as an expression by assigning it to a varible. If no error occurs, you've got an expression:
// Simple values
42
// Constructor calls
new Map()
// Assignment operations (chained assignments are valid)
a = b = 15
// Conditional expressions
temperature > 30 ? "hot" : "cold"
// Function expressions
const fn = function() { return 42; }
// Identifiers (variables)
counter
JavaScript categorizes expressions into several types:
- Primitive expressions: Literals, keywords, and variables
- Literal expressions: Array and object literals
- Function definition expressions: Anonymous and named functions
- Property access expressions: Dot notation and bracket notation
- Call expressions: Function invocations with parentheses
- Object creation expressions: Constructor calls with
new
Precedence, Associativity, and Evaluation Order
MDN defines precedence as the rule that determines evaluation order for operators in an expression. But there's another concept at play: associativity.
Consider these examples:
let result = !true && false;
let sum = 1 + 2 + 3;
In the first example, !true is evaluated before the && operation. In the second, additions occur from left to right. These behaviors aren't coincidental—they're governed by precedence and associativity rules.
Precedence dictates that higher-priority operators bind more tightly to their operands, essentially as if wrapped in invisible parentheses. Associativity handles operators with equal precedence, specifying whether they group left-to-right or right-to-left.
Let's revisit the Date example that caused confusion:
// Grouping interpretation: (new Date()) followed by .getFullYear()
new Date().getFullYear()
// Grouping interpretation: new (Date.getFullYear()) which throws an error
new Date.getFullYear()
The error in the second form stems from precedence. According to MDN's operator precedence table:
- The
newoperator with parentheses (likenew Date()) has precedence level 19 - The
newoperator without parentheses (likenew Date) has precedence level 18 - Property access with
.has precedence level 19
This precedence difference explains why the two forms behave differently.
Associativity in action:
// Right-associative: equivalent to result = (!(value))
let enabled = !false;
// Left-associative: equivalent to ((1 + 2) + 3) + 4
let total = 1 + 2 + 3 + 4;
// Right-associative ternary: equivalent to x ? a : (y ? b : c)
let status = x ? "yes" : y ? "pending" : "no";
Operators and Expression Building
At its core, an operator combines expressions to form new expressions:
// Primitive expression
42
// Unary operator applied to expression
!42
// Binary operator combining two expressions
10 + !42
// Ternary operator combining three sub-expressions
isValid ? successValue : errorValue
Operators essentially "glue" nearby expressions together, building increasingly complex expressions until a final value emerges.
Further Exploration
The rules governing operator precedence and associativity form the foundation for understanding how JavaScript interprets complex expressions. Once these concepts become intuitive, code that initially appeared cryptic becomes readable.
Consider analyzing this expression to test your understanding:
(+![] + ![])[+!+[]] + ([]+{})[+!+[]]
Breaking it down using precedence rules reveals its structure and ultimate output.
To deepen your knowledge:
- Study the complete operator precedence table on MDN
- Practice by manually parsing complex expressions before testing them
- Build abstract syntax trees mentally to understand evaluation order