JavaScript 逗号运算符

199 阅读7分钟

(на русском, 日本)
 
Let’s begin with a funny tweet:

The ‘c’ at the end is for the lowly comma operator. Last in the line of operator precedence and rarely documented, the comma operator hides its light under a bushel. It may not be a JavaScript heavy-hitter but I like it anyway. Its simple, elegant and you should make it your friend. So, here we go – more than you’ll ever need to know about JavaScript’s bashful hero:

What does it do?

The comma operator evaluates both of its operands (from left to right) and returns the value of the second operand. (MDC)

var a = (7, 5);
a; //5
 
var x, y, z
x = (y=1, z=4);
x; //4
y; //1
z; //4

 
Why did you wrap those variable assignments in parentheses?

Because of operator precedence. A JavaScript statement can contain multiple, disparate operators. The following statement has three operators (*, + and ,) :

return 5 * 2 + 3,  22;

 
Operator precedence determines the order in which operators are evaluated within a statement. The full list, in order of precedence is here. The comma operator has the lowest precedence of any operator. Lets simulate how this applies to the above example:

//original
return 5 * 2 + 3,  22;
//apply * operator
return 10 + 3,  22;
//apply + operator
return 13, 22;
//apply , operator
return 22;

 
Now let’s use that knowledge to see what would happen if we hadn’t wrapped the variable assignment in parentheses:

//original
var a = 7, 5;
//apply = operator
var a, 5; //a is now 7
//SyntaxError: missing variable name 

 
By wrapping the right hand expression in parentheses we create a group – which, effectively has the highest precedence. This ensures that the comma operator gets applied first:

//original
var a = (7, 5);
//apply group
var a = 5; 

 
In practice, lowest operator precedence actually makes the comma operator quite powerful. In effect it says: go ahead and see to all those other little operations first, then watch me come and clobber the result.

Some statements contain multiple commas. How does that work?

The above rule still applies. Each comma operator in the statement is processed in sequence from left to right.

var a = (1, 2, 3, 4);
a; //4

 
This is equivalent to:

var a = (((1, 2), 3), 4);
a; //4

 
What about commas used in type literals and declarations?

These are comma separators not comma operators. The purpose of a comma separator is to delimit members in a list. For example:

//set 4 array elements
var arr = [1, 2, 3, 4];
 
//create an object with 2 properties
var obj = {
  a: 22,
  f: function() {return this.a*this.a}
}
 
//define 3 distinct variables
var a = 1, b = 2, c = 3;
 
//invoke a function passing 2 arguments
<span class="skimlinks-unlinked">Math.max(4</span>, 7);

 
Why use comma operators?

Because they let you specify more than one expression where JavaScript expects only one. Comma operators are rarely essential but often useful and just occasionally downright elegant:

var r = [], n = 0, a = 0, b = 1, next;
 
function nextFibonacci() {
    next = a + b;
    return b = (a = b, next);
}
 
while(n++ < 10) {
    r.push(nextFibonacci());
}
 
r; //[1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

 

function getRandomPrime() {
    while(n = <span class="skimlinks-unlinked">Math.round(Math.random()*1000000000</span>), !isPrime(n));
    return n;
}
 
var isPrime = function(n) {
    d = <span class="skimlinks-unlinked">Math.ceil(Math.sqrt(n</span>));
    while(n%(d--) && d);
    return !d;
}
 
getRandomPrime(); //425593109
getRandomPrime(); //268274719

 
Isn’t the comma operator just a semicolon in disguise?

Semicolons partition statements. Comma operators partition expressions within statements.

Why wouldn’t I just use the && operator to evaluate multiple expressions sequentially?

The comma operator is a close cousin of the && and || operators. All three operators will return the last expression they evaluate. The distinction is straightforward:

//(LHE: left hand expression, RHE right hand expression)
 
LHE && RHE
1. Always evaluate LHE
2. If LHE is true, evaluate RHE
 
LHE || RHE
1. Always evaluate LHE
2. If LHE is false, evaluate RHE
 
LHE, RHE
1. Always evaluate LHE
2. Always evaluate RHE

 
Choose the comma operator when both expressions must always be evaluated.

How about some more examples?

Okay. Earlier on I mentioned that comma operators let you specify more than one expression where JavaScript expects only one. This is perhaps most useful within the confines of the for loop:

for loops

Here’s an alternate version of a fibonacci generator, also using the comma operator:

for (
    var i=2, r=[0,1];
    i<15;
    r.push(r[i-1] + r[i-2]), i++
); 
 
r //"0,1,1,2,3,5,8,13,21,34,55,89,144,233,377" 

 
For another example, consider a utility that helps a store clerk select the bills and coins that make up a customer’s change. Here’s the basic version. We use a comma operator to bisect the second statement of the for loop. This lets us neatly increment our currency counter before testing against the limiting expression:

function toCurrency(total, values) {    
    total *= 100;     
    for(        
        var i=0,counts=[];
        counts[i]=total/values[i], total=total%values[i];
        i++
     );     
     return <span class="skimlinks-unlinked">counts.map(Math.floor</span>); 
} 
 
toCurrency(32.47, [500, 100, 25, 10, 5, 1]); //[6, 2, 1, 2, 0, 2]

 
Now here’s the same utility with added formatting for user-friendliness:

function toCurrency(total, values, sym) {
    total *= 100;     
    //do the calc     
    for(
        var i=0,counts=[];
        counts[i]=total/values[i], total=total%values[i];
        i++
    );     
   //format
   var results = <span class="skimlinks-unlinked">counts.map(function(s,i</span>) {
       return s>=1 && [<span class="skimlinks-unlinked">Math.floor(s),"x",(sym</span>
            (values[i]/100).toFixed(2)].join(' ');
    });
    return results.filter(Boolean).join(', ');
}
 
toCurrency(19.77, [500,100,25,10,5,1]);
//"3 x $5.00, 4 x $1.00, 3 x $0.25, 2 x $0.01"
toCurrency(19.77, [500,100,50,20,10,5,1], '£');
//"3 x £5.00, 4 x £1.00, 1 x £0.50, 1 x £0.20, 1 x £0.05, 2 x £0.01"
toCurrency(19.77, [500,100,50,20,10,5,2,1], '€');
//"3 x €5.00, 4 x €1.00, 1 x €0.50, 1 x €0.20, 1 x €0.05, 1 x €0.02"

 
This following function uses the comma operator to simultaneously increment and decrement two counters within a for loop. The product of the counters is used to render a rather fetching curve in the console:

function renderCurve() {
  for(var a = 1, b = 10; a*b; a++, b--)
    <span class="skimlinks-unlinked">console.log(new</span> Array(a*b).join('*'));
}
 
renderCurve();
/*
*********
*****************
***********************
***************************
*****************************
*****************************
***************************
***********************
*****************
*********
*/

 
while loops

You can use a comma operator to create a succinct version of the do-while loop. This routine searches an elements ancestry looking for a tag name match. Again we use the comma to perform an action prior to checking the limiting expression:

function firstAncestor(el, tagName) {
  while(el = el.parentNode, el && (el.tagName != tagName.toUpperCase()));
  return el;
}
 
//element in http://ecma262-5.com/ELS5_HTML.htm
var a = $('Section_15.1.1.2'); 
 
firstAncestor(a, 'div'); //<div class="page">

 
Ternary conditionals

Ternary syntax allows for only one statement in each of its three components. As a general rule, if you need to use more statements you should consider using if else instead. However it’s sometimes more readable when the comma operator is used to combine short succinct expressions within a ternary statement:

//player loses
lives ? (lives--, go()) : (gameOver(), exit());

 
Debugging

The comma operator provides an unobtrusive way to inject console logs into your code without having to reformat (can you spot the errors that necessitated debugging in each case?)…

//CONTAINS AN INTENTIONAL ERROR!!!
//sum products while i > n
var i=10, n=0, total=0;
while(<span class="skimlinks-unlinked">console.log(i,n</span>), i-- > n++); {
    total += i*n
}
//CONTAINS AN INTENTIONAL ERROR!!!
//sum an array
var arr = [1,2,3];
for (
    var i=0, total=0;
    i<arr.length;
    <span class="skimlinks-unlinked">console.log(i,total</span>), total += arr[i++]);
)
//CONTAINS AN INTENTIONAL ERROR!!!
//add 4 to members of array and sum it
//(yes there are easier ways to do this!)
var testArray = [3, 5, 8, 4], total = 0;
var plusFour = <span class="skimlinks-unlinked">testArray.map(function(e</span>) {e + 4})
plusFour.forEach(function(n) {<span class="skimlinks-unlinked">console.log(n</span>), isNaN(n) || (total += n)});

 
Binding with iterators

@wavded posted this nifty technique for unobtrusively resetting iterators. Again, you don’t need to do it this way – but the tidiness appeals to me:

var colorIndex = 0, 
    colors = ["FF0000", "008000", "FF0086", "A2FF00", "0000FF", "800080"]; 
 
function selectNextColor(){
    return colors[colorIndex++] || colors[colorIndex = 0, colorIndex++];
}

 
Indirect calls to eval

eval¹ calls are normally invoked within their containing context (i.e. the this value in the evaluated code will be the same as the the this value of the surrounding code). This is problematic since there is no guarantee that repeated eval calls will originate in the same context.

As @kangax describes here, we can use the comma operator to fashion an indirect call to eval which will force it to execute in the global context²:

var a = {};
 
//attempt eval in context of object <code>a</code>
(function() {
    eval("<span class="skimlinks-unlinked">this.alert('If</span> can read this I must be global!')");
}).call(a);
//TypeError: <span class="skimlinks-unlinked">this.alert</span> is not a function
 
//force eval in global context
(function() {
    (0,eval)("<span class="skimlinks-unlinked">this.alert('If</span> can read this I must be global!')");
}).call(a);
//alerts: 'If you can read this I must be global!'

 
¹ discussion of the merits of eval are beyond the scope of this article 😉
² although the ES5 standard confirms that indirect calls to eval should run in the global context, not every browser is compliant (i.e. IE <= 8).

Wrap Up

You could probably write perfectly good JavaScript code without ever using the comma operator. Does this mean I just wasted your time? I hope not. Just as an extensive vocabulary makes us better speakers and writers, so a comprehensive access to language features should make us better coders. The more techniques we have at our disposal the greater our ability to write elegant, succinct and readable code. Have fun with comma operators and please share your neat usage examples!

Further Reading

ECMA-262 5th Edition
    11.14 The comma operator
    10.4.2 Entering eval code
    15.1.2.1.1 Direct Call to Eval

Mozilla Developer Center
    comma operator
    operator precedence

Juriy Zaytsev (@kangax): global eval, what are the options
Mark Harter (@wavded): cycling through an array using the comma operator

Advertisements