DEV Community

EmNudge
EmNudge

Posted on

The Comma Operator and Operator Precedence in JavaScript

Let's take a look at an expression. Give you best guess for what it evaluates to:

    40 + 36,000
Enter fullscreen mode Exit fullscreen mode

Some might say 76000, others might say it'll throw an error. In actuality, this expression is completely valid in JS and it evaluates to 0. How? The comma operator.

The comma operator is the operator with the weakest "binding" in JS. Remember PEMDAS from math class? Operators have their own order of operations, but it's instead referred to as bindings or "Operator Precedence".

MDN describes precedence as "Operators with higher precedence become the operands of operators with lower precedence". What this means is that if an operator (which has 2 operands) has a higher precedence, it is as if it is surrounded by parentheses; it is more strongly bound to the values to its right and/or left.

    40 + 36,000                          // 0
    (40 + 36) , 000                      // 0

    new Date().getDay() + 3 * 2          // some number  6 - 12
    (((new Date()).getDay)()) + (3 * 2)  // the same number

    ! void false && true || false        // true
    !((void (false && true)) || false)   // true

    true ^ ! 100 < 56 ? 2 : 1             // 3
    true ^ (((! 100) < 56) ? 2 : 1)       // 3
Enter fullscreen mode Exit fullscreen mode

The levels of precedence for each is listed on the MDN page near the bottom, right after the example. If 2 operators have the same precedence, their associativity tells us whether to go right-to-left or left-to-right when evaluating them.

The comma operator has the lowest precedence. It binds last. If you ever see a comma, you know it won't accidentally bind to some code that it shouldn't.

So what does the comma do? It takes 2 expressions as operands and returns the rightmost one. It is left-to-right associative and we can therefore chain them. Here are some examples

    5 , 2               // 2
    3 , 4 * 2           // 8
    40 , 5 , 1 , false  // false 
Enter fullscreen mode Exit fullscreen mode

Note that it specifically takes in expressions, not statements. That means we can't put things like let x = 4 as one of the operands, but we can put things like x = 4. Declarations are statements, while assignments are expressions.

What else is an expression? decrements, increments, and Function calls!

This means the following is valid JS:

    let x;
    const myFunc = num => {
        for (let i = 0; i < num; i++) console.log(i);
    } 

    const result = (x = 3, x++, myFunc(x), x * 2);
    console.log(`result: ${result}`);

    // logs:
    // 0
    // 1
    // 2
    // 3
    // result: 8
Enter fullscreen mode Exit fullscreen mode

Parentheses are required as the comma operator's low precedence would make us accidentally combine the assignment and the first expression. Parentheses have the highest precedence, so they contrast nicely to the comma's lowest precedence.

The one case where parentheses are not required is when one approaches an "operator" of even higher precedence, the semicolon.

This allows us to leave the parentheses behind where semicolons are involved, such as in loop headers.

    let i;
    for (i = 0, i++, i--; i < 10, i < 5; i += 2, i-= 1) {
        console.log(i)
    } 

    // logs:
    // 0
    // 1
    // 2
    // 3
    // 4
Enter fullscreen mode Exit fullscreen mode

How is any of this useful? One of my favorite way is code golfing. As long as we don't involve any statements, we can stuff an entire loop body into the header.

    for (let i = 0; i < 3; console.log('hey!'), i++);

    // logs:
    // hey
    // hey
    // hey
Enter fullscreen mode Exit fullscreen mode

If we have a variable that is already defined and initialized, we can also just use a while loop.

    let i = 0;
    while(console.log('hey!'), ++i < 3);

    // logs:
    // hey
    // hey
    // hey
Enter fullscreen mode Exit fullscreen mode

One of the situations which the comma operator would have been especially useful is for a replacement for a function body when it comes to arrow functions. Unfortunately, the comma operator has an even lower precedence than => and so we require parentheses to separate them. This means we're not saving any characters, as we're using () and , instead of {} and ;.

It is only useful in arrow functions if you intend on returning a value, in which you are missing out on a return statement due to the arrow function's implicit return when no function body is present.

    const myFunc1 = () => (console.log("func1 called"), 3);
    const myFunc2 = () => {console.log("func2 called"); return 3}

    console.log(myFunc1());
    console.log(myFunc2());

    // logs:
    // func1 called
    // 3
    // func 2 called
    // 3
Enter fullscreen mode Exit fullscreen mode

Consclusion

I hope you learnt a bit more about operator precedence and the power of manipulating it. I often see people grouping operators where they otherwise wouldn't have to. Grouping operators can often make code more readable, but too many can just as well bring about expression and operator soup.

Use your new found power wisely.

Top comments (0)