Оператор запятая

http://javascriptweblog.wordpress.com/2011/04/04/the-javascript-comma-operator/
  • Перевод
Продолжаем тему операторов, на этот раз вас ждет рассказ об операторе запятая.

Начнем с забавного твита:

Если изображение не отображается, пожалуйста, напишите автору!

'c' в конце — это оператор запятая. Последняя в списке значимых операторов, мало документированная, но очень полезная. Она не так распространена, но она мне очень нравится. Она проста, элегантна, и с ней лучше быть в хороших отношениях.

Что она делает?


Оператор запятая выполняет оба операнда (слева направо) и возвращает значение второго оператора. (MDC)
var a = (7, 5);
a; //5

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

Почему в вашем примере присваивания переменных окружены круглыми скобками?


Из-за приоритета операторов. Выражение JavaScript может содержать несколько различных операторов. Следующее выражение содержит три оператора (* + и ,):

return 5 * 2 + 3,  22;

Приоритет операторов определяет, в каком порядке будут выполнены операнды внутри выражения.
Полный список операторов тут. Оператор запятая имеет наименьший приоритет из всех операторов. Давайте посмотрим на примере:

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

Сейчас давайте посмотрим что будет если мы уберем круглые скобки:

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

Обрамляя выражение круглыми скобками, мы создаем группу, которая имеет наивысший приоритет.
Это гарантирует, что оператор запятая будет применен в первую очередь.

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

На практике, благодаря своему низкому приоритету, запятая — мощное средство. Фактически, она говорит интерпретатору: сначала посмотри, что же делают все остальные операторы вокруг меня, а потом позволь мне украсить собой результат.

Некоторые выражения содержат несколько запятых. Как это работает?


Каждый оператор в цепочке обрабатывается последовательно слева направо.

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

Это эквивалентно:

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

Что насчет запятых, использующихся в литералах типа и в объявлениях?


Эти разделители на самом деле не операторы запятая. Назначение разделителя-запятая — разделение членов в списке. К примеру:

// создает массив из 4 элементов
var arr = [1, 2, 3, 4];

//создает объект с двумя свойствами
var obj = {
  a: 22,
  f: function() {return this.a*this.a}
}

//определяет три переменные
var a = 1, b = 2, c = 3;

//вызывает функцию, передавая 2 параметра
Math.max(4, 7);

Зачем использовать оператор запятая?


Затем, что она позволяет вам выполнить несколько выражений в том месте, где JavaScript ожидает только одно. Выражения с оператором запятая не такие распространенные, редко важные, но очень элегантные:

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 = Math.round(Math.random()*1000000000), !isPrime(n)); // <<< Вот тут
    return n;
}

var isPrime = function(n) {
    d = Math.ceil(Math.sqrt(n));
    while(n%(d--) && d);
    return !d;
}

getRandomPrime(); //425593109
getRandomPrime(); //268274719

Разве точка с запятой — не замаскированная запятая?


Точка с запятой — это разделитель объявлений, а запятая — это разделитель выражений внутри объявлений.

Почему бы не использовать оператор && для выполнения нескольких выражений последовательно?


Оператор запятая очень близка к оператору && и ||. Все эти операторы возвращают последнее выражение, которое они выполнили. Вот в чем их различие:

LHE — левое выражение
RHE — правое выражение

LHE && RHE
1. Всегда выполняет LHE
2. если LHE — true, выполняет RHE

LHE || RHE
1. Всегда выполняет LHE
2. Если LHE — false, выполняет RHE

LHE, RHE
1. Всегда выполняет LHE
2. Всегда выполняет RHE

Вам следует выбрать запятую, если оба выражения должны быть выполнены.

Как насчет примеров?


Как я отмечал ранее, оператор запятая позволяет вам выполнить несколько выражений в том месте, где JavaScript ожидает только одно.

Циклы for

Вот альтернативная версия генератора чисел Фибоначчи, которая также использует оператор запятая:

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"

Для другого примера рассмотрим утилиту, которая помогает продавцу выбирать купюры и монеты, составляющие сдачу покупателя:

function toCurrency(total, values) {
    total *= 100;
    for(
        var i=0,counts=[];
        counts[i]=total/values[i], total=total%values[i]; // Вот тут
        i++
     );
     return counts.map(Math.floor);
} 

toCurrency(32.47, [500, 100, 25, 10, 5, 1]); //[6, 2, 1, 2, 0, 2]

Та же самая утилита, но с форматированием:

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 = counts.map(function(s,i) {
       return s>=1 && [Math.floor(s),"x",(sym || '$') +
            (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"

Следующая функция использует запятую для одновременного увеличения и уменьшения двух переменных внутри цикла. На выходе получаем кривую:

function renderCurve() {
  for(var a = 1, b = 10; a*b; a++, b--) // Вот тут
    console.log(new Array(a*b).join('*'));
}

renderCurve();
/*
*********
*****************
***********************
***************************
*****************************
*****************************
***************************
***********************
*****************
*********
*/

Циклы while

Вы можете использовать оператор запятая для создания кратких версий циклов do-while.
Эта функция ищет предка из списка элементов с именем tagName (аналог jQuery parent).

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">

Тернарный оператор

Тернарный оператор позволяет выполнять только одно выражение. Если вам необходимо выполнить несколько выражений, то вам приходится переходить на if else. Оператор запятая более читаемый в тех случаях когда он используется для комбинации коротких выражений:
//player loses
lives ? (gameOver(), exit()) : (lives--, go());

Дебаг

Оператор запятая позволяет вам вставлять console.log в любое место без изменения кода:

//складывает products пока i > n
var i=10, n=0, total=0;
while(console.log(i,n), i-- > n++); {
    total += i*n
}


//складывает элементы массива
var arr = [1,2,3];
for (
    var i=0, total=0;
    i<arr.length;
    console.log(i,total), total += arr[i++]);
)


//добавляет 4 к каждому элементу массива и складывает их
var testArray = [3, 5, 8, 4], total = 0;
var plusFour = testArray.map(function(e) {e + 4})
plusFour.forEach(function(n) {console.log(n), isNaN(n) || (total += n)});

Связывание с итераторами

@wavded опубликовал один способ применения запятой.

var colorIndex = 0,
    colors = ["FF0000", "008000", "FF0086", "A2FF00", "0000FF", "800080"]; 

function selectNextColor(){
    return colors[colorIndex++] || colors[colorIndex = 0, colorIndex++];
}

Косвенный вызов eval

eval1 использует тот контекст в котором был вызван. Поэтому нет гарантий, что вызов eval в цикле даст тот же самый результат.

kangax написал, что мы можем использовать оператор запятая для косвенного вызова eval, который будет всегда вызываться в глобальном контексте2:

var a = {};

(function() {
    eval("this.alert('If you can read this I must be global!')");
}).call(a);
//TypeError: this.alert is not a function

(function() {
    (0,eval)("this.alert('If you can read this I must be global!')");
}).call(a);
//alerts: 'If you can read this I must be global!'

1. Пожалуйста без холиваров, все знают, что eval — evil
2. В стандарте ES5 написано, что любой не прямой вызов eval использует глобальный контекст,
однако не все браузеры поддерживают это правило (IE<=8)

Заключение


Вы можете писать отличный код и без использования оператора запятая. Значит ли это, что я потратил ваше время? Я надеюсь, что нет. Обширный словарный запас делает писателей и ораторов более профессиональными так и доступ к широком возможностям языка может сделать из нас лучших кодеров. Чем больше методов мы знаем тем более красивый, аккуратный и читаемый код мы можем написать. Удачи с оператором запятая, поделитесь своими примерами использования!

Почитать


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

UPD Принял во внимание и исправил недочеты в переводе
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 49
  • –1
    Просветили, спасибо!
    • –3
      Спасибо большое!
      • –5
        Когда читал английскую версию это звучало как Comma оператор и я из за моего Bad English подумал то что есть какой то оператор из за которого движок js виснет но когда прочитал статью все встало на свои места
      • 0
        Только сегодня наконец-то прочитал эту статью в оригинале, а тут уже перевод.
        • +13
          Оператор очень запятая близка к оператора

          «Очень запятая» это более крутой оператор чем обычная запятая :D

          З.ы.: И, вообще, начало предложения странное.
          • +6
            Оператор очень запятая близка к оператора и его жена были в основном из за того что я не могу понять как это сделать в домашних условиях и в порядке надзора судебных актов отказано в связи с чем в настоящее время в России в начале ХХ века в России было продано около двух лет назад в России был принят закон о запрете курения в общественных местах и на улицах города и в его окрестностях в том числе и в России в начале ХХ века
            (с) scribe.googlelabs.com
        • +1
          Спасибо! Не знал таких подробностей. Еще одна техника нинздя-кодера :)
          • +21
            «Чем больше неочевидных конструкций языка я задействую — тем незаменимей я стану»
            • +14
              Ниндзя — это хорошо. Навыки ниндзи очень пригодятся такому кодеру при встрече с человеком, которому доведется поддерживать такой код.
              • 0
                Если не применять навыки ниндзя при написании комментариев, то скорее всего не пригодятся.
              • НЛО прилетело и опубликовало эту надпись здесь
                • +1
                  А мне казалось, что любой, кто знает Си (с которым культурно связан жаваскрипт) знает этот оператор.
              • 0
                Оператор запятая выполняет оба оператора (слева на право) и возвращает значение второго оператора.
                Не оператора, а операнда и лучше сказать «вычисляет».
                С этим, кстати, связан такой довольно забавный пример:
                alert( typeof(someUndefinedValue) )
                alert( typeof(window, someUndefinedValue) )


                Как можно видеть, оператор группировки (в простонародье скобки) не вычисляют значения (используемая мною тут терминология несколько отличается от той, что применяется в стандарте) операнда. Поэтому (eval)('la-la-la') и eval('la-la-la') эквиваленты. Зато (0, eval) заставит содержимое скобок вычислиться и в результате мы получим eval, «открепленный» от своего контекста по-умолчанию.
                • +5
                  Назначение разделителя запатая — разделение членов в списке.
                  • –1
                    statement = оператор (присваивания, условный, цикла и т.д).
                    operator = операция (сложения, возведения в степень, и т.д).
                    • 0
                      Operator Comma ближе к Оператор Запятая по аналогии с Оператор побитового сдвига влево
                      statement
                      1) утверждение; высказывание; формулировка
                      2) оператор; предложение
                      operator
                      1) оператор(см. тж statement); знак операции
                    • +23
                      Знание оператора запятая это, конечно, хорошо. Но если вместо

                      return colors[colorIndex++] || colors[colorIndex = 0, colorIndex++];

                      вы напишете

                      return colors[(colorIndex++) % colors.length];

                      то тот, кто будет поддерживать этот код, будет реже вспоминать вас матом.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • +13
                          Любой дурак может написать код, понятный компьютеру. Хороший программист пишет код, понятный человеку.
                          Мартин Фаулер
                          — Ну и «Пишите код, ожидая что его будет поддерживать с замашками маньяка и садиста»
                          • +1
                            «знающего ваш домашний адрес». Это важно.
                          • –2
                            У Пола Айриша в блоге как-то встретил:
                            // Detect IE in JS using conditional comments
                            var ie = (function(){
                             
                                var undef,
                                    v = 3,
                                    div = document.createElement('div'),
                                    all = div.getElementsByTagName('i');
                             
                                while (
                                    div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',  // <<< Вот тут
                                    all[0]
                                );
                             
                                return v > 4 ? v : undef;
                             
                            }());
                            

                            Автор
                            • +3
                              Ужас. Для определения IE придумано множество других приемов, которые гораздо короче в написании и работают, не затрагивая DOM модель.
                              • –1
                                Хм… А сколько из них не используют UA?
                                • +1
                                  Много. Впрочем, я не заметил то, что этот скрипт еще и версию определяет, а альтернативных, не использующих DOM, я не знаю.
                                  • –1
                                    Все User Agent известны почему бы их не использовать.
                                    В JavaScript где, в отличии от css/html, возможности велики наоборот отходят от хаков: см. jQuery, UglifyJS 'v'=='\v' всегда переделывает в !1 (Won't Fix bug)…
                                    Просто рано или поздно эти баги/квирксы могут пофиксить (прим. IE9 'v'=='\v'), а вот форма UA не поменяется.
                              • +1
                                В С++ его еще и перегружать можно, что ведет к очень интересным эффектам, к сожалению, в большинстве своем это рождает говнокод.
                                • 0
                                  Опа, интересная заметка. Спасибо.

                                  Только как интерперататор будет отличать оператор от разделителя?

                                  Например, в следующем вызове я создаю массив с 2-мя элементами 5 и 7 или массив длиной 7?

                                  var a = new Array(5,7);
                                  • 0
                                    Вот из-за таких вопросов лучше создавать масивы так:
                                    var a = [5, 7]; //два элемента
                                    var a = Array(n); //n — элементов
                                    и никогда не смешивать :)
                                    • +1
                                      А тут в чем разница?
                                      То ли [5, 7] это [(5, 7)]
                                      То ли [5, 7] это [5, 7]

                                      А вот тут нам на помощь приходит приоритет операторов. У запятой самый низкий приоритет из всех операторов, а у квадратных скобок и вызовов функций (скобки в Array() и просто скобки () это не одно и то же) гораздо выше.
                                      • 0
                                        да, что скобки и создание нового объекта не одно и то же — понятное дело. Но ваш ответ прояснил ситуацию — дело именно в приоритете, спасиб )
                                        • 0
                                          Извините, мое предыдущее сообщение не соответствует действительности.

                                          Тут нет оператора запятой. Я ошибался и дело не в приоритете, а в том, что Array(...) — вызов функции, а [...] — инициализатор массива. Они оба принимают список аргументов с запятой в качестве разделителя. А вот [] как оператор индексирования хоть и имеет больший приоритет, но на операторе-запятой это никак не сказывается.

                                          alert([3,4,5,6][1,2])
                                          В этой записи [3,4,5,6] — инициализатор массива, а [1,2] — оператор индексирования (доступа к свойствам), у которого «аргумент» (операнд) — простое выражение, в вычислении которого участвует оператор-запятая (в первом случае ей было неоткуда взяться, т.к. по ней разбивается список аргументов; исключение составляет выражение в скобках).
                                          • 0
                                            да, я тоже ошибся, приняв приоритет за правильный ответ. Контекст — вот более правильно.

                                            Но вопрос тогда остался открытым (я его задал ниже) — как интерпретатор JS отличает в подобных местах, что конструкцию следует рассматривать именно как перечисление двух аргументов, а не как команду обсчитать выражение, в котором участвует оператор запятая. Ведь выражение является валидным аргументом, а оператор запятая — валидна в выражении.
                                            • 0
                                              Как я понял из беглого просмотра стандарта,
                                              Список аргументов разбивается по запятым на особые выражения (ВыражениеПрисваивания в документе по ссылке выше), которые не могут содержать оператор-запятую (Зато могут содержать оператор группировки (скобки), которые уже могут содержать выражения (Выражение в терминах стандарта) с оператором-запятой). Поэтому никаких проблем с неопределенным смыслом не возникает. А вот для оператора-группировки «аргумент» (операнд) является Выражением, т.е. позволяет использовать запятую.
                                    • 0
                                      Тут дело не в запятой, а в поведении конструктора Array.
                                      В вашем случае создается массив [5, 7] потому, что конструктор Array имеет следующую логику:
                                      1. Если arguments.length === 1 && typeof arguments[0] === 'number' && (arguments[0] < 0 || isNaN(arguments[0]) && !isFinite(arguments[0])), то выбрасывается исключение RangeError
                                      2. Иначе если arguments.length === 1 && typeof arguments[0] === 'number', то создается массив длиной arguments[0]
                                      3. Иначе создается массив из arguments:
                                      new Array(Infinity); // RangeError
                                      new Array(-1); // RangeError
                                      new Array(NaN); // RangeError
                                      new Array(NaN, NaN); // [NaN, NaN]
                                      new Array(undefined); // [undefined]
                                      new Array(1); // [undefined]
                                      new Array(0); // []
                                      new Array(0, 0); // [0, 0]
                                      
                                      • –1
                                        хехе, вы попались на ту же удочку, что и pietrovich выше :) дело в том, что тут уже не важно, какая логика внутри конструктора, так как тут вопрос о том — что за аргументы передаст этой логике конструкция new Array(5, 7).

                                        Чуть выше ситуация прояснена — дело именно в приоритете, так как запятая самая низкоприоритетная, то тут сработает оператор вызова конструктора и передаст содержимое скобок как 2 аргумента.
                                        • 0
                                          хехе, вы попались на ту же удочку
                                          Чуть выше ситуация прояснена — дело именно в приоритете, так как запятая самая низкоприоритетная, то тут сработает оператор вызова конструктора и передаст содержимое скобок как 2 аргумента
                                          Если бы тут был именно Оператор Запятая, а не Разделитель Запятая, то по своей логике (выполняет все операнды и возвращает последний) в массив бы она передала только 7. Помоему, это Вы плохо читали.

                                          Вы утверждаете, что и тут запятая — Оператор Запятая: Math.min(1, 4, 5)?
                                          • 0
                                            Вы правы, здесь действительно нет оператора-запятой.
                                            Моя вина, ввел хабраюзера Zerkella в заблуждение.
                                            • 0
                                              Не переживайте, все ок. Идею я понял — дело в контексте.
                                          • –1
                                            Передаст 5 и 7 потому, что запятая это Разделитель Запятая, а не Оператор Запятая, а скобки это Function Invoke выражение, а не оператор группировки.
                                            Я исчерпывающе ответил на ваш вопрос выше:
                                            Например, в следующем вызове я создаю массив с 2-мя элементами 5 и 7 или массив длиной 7?
                                            var a = new Array(5,7);
                                            • 0
                                              Не сердитесь вы так сильно :)

                                              Лучше подскажите, по каким правилам технически при разборе текста интерпретатор отличает оператор запятую от разделителя запятой?

                                              • 0
                                                Пример:
                                                alert
                                                ( ( 2 * 2, 0 ), 2);
                                                Интерпретатор удаляет все ненужные невидимые символы, ставит где это необходимо запятые, проводит валидацию.
                                                alert((2*2,0),2)
                                                1. Интерпретатор «видит» alert — эта конструкция подходит по описание переменной
                                                2. Видит, что после переменной стоят скобки — первые скобки Function Invoke выражение.
                                                3. Из-за того, что скобки Function Invoke выражение, то воспринимает запятую перед двойкой как Разделитель Запятая (перечисляет список переменных функции).
                                                4. Начинает по очереди резолвить переменные.
                                                4.1. Доходит до первого аргумента (2*2,0) — это выражение. Исходя из приоритета операций (сперва () затем * затем ,) резолвит это выражение — получаем первый аргумент = 0.
                                                4.2. Доходит до второго аргумента — константа (резолвить не надо) = 2.
                                                5. Далее резолвит объект на который указатель alert смотрит (находит только в global scope) если объекта нет, то выдает ошибку ReferenceError.
                                                6. Смотрит, что объект на который ссылается alert есть Callable — вызывает Callable Object aka Function на который ссылается alert с параметрами 0, 2. Если он не Callable, то выбрасывает ошибку TypeError.
                                                7. Дальнейшее поведение alert всем известно.

                                                В зависимости от интерпретатора он может не убрать точку с запятой, тогда в довесок вызовется ещё и «пустой оператор».
                                      • 0
                                        Что-то не очень оно элегантно [читабельно] — записывать несколько выражений в одну строчку, вместо многих, что показано в большинстве Ваших примеров.
                                        • +1
                                          Еще один пример использования оператора запятая — вызов Function Expression.

                                          Function Declaration (будет ошибка):
                                          function(){}()

                                          Function Expression (как в jQuery):
                                          (function(){})()

                                          Function Expression (с оператором запятая):
                                          1, function(){}()

                                          P.S. На практике я бы использовать не стал :)
                                          • 0
                                            я вот даже не знаю, что лучше — Function Expression (как в jQuery) или Function Expression (с оператором запятая). Мне не очень нравится количество скобок в конце такого выражения:
                                            (function () {
                                                // code
                                            })();
                                            

                                            Пока использую через new, но смущает отсутствие глобального контекста в this.
                                            new function () {
                                                // code
                                            };
                                            
                                            • 0
                                              Facebook Style
                                              !function (window, document) {
                                                  // code
                                              }(this, document);
                                              
                                          • 0
                                            Кстати, для выполнения кода в глобальном контексте можно создавать функции через конструктор Function:

                                            var a = {};
                                            (function() {
                                                new Function("this.alert('If you can read this I must be global!')")();
                                            }).call(a);
                                            • 0
                                              Вот как полезно писать «забавные» твиты, оказывается. :)

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.