Зачем избегать увеличения («++») и уменьшения («-») операторов в JavaScript?

357

Один из советов для инструмента jslint :

++ и -
Операторы ++ (увеличение) и - (уменьшение), как известно, вносят свой вклад в плохой код, поощряя чрезмерную хитрость. Они уступают только неисправной архитектуре в плане защиты от вирусов и других угроз безопасности. Существует опция plusplus, которая запрещает использование этих операторов.

Я знаю, что подобные конструкции PHP $foo[$bar++]могут легко привести к ошибкам, но я не мог найти лучшего способа управления циклом, чем while( a < 10 ) do { /* foo */ a++; }или for (var i=0; i<10; i++) { /* foo */ }.

Подсвечивает ли jslint их, потому что есть некоторые похожие языки, в которых отсутствует синтаксис « ++» и « --» или они обрабатываются по-разному, или есть другие причины для избежания « ++» и « --», которые я мог бы пропустить?

artlung
источник
59
Таким образом, вместо массива [++ index] следует использовать массив [index = index + 1] (если первый даже разрешен!). Что за хуй!
Лоуренс Дол
34
Я не видел, чтобы Крокфорд делал index = index + 1. Я видел, как он сделал индекс + = 1. Я думаю, что это разумная альтернатива. И это хорошо, когда вы хотите изменить шаг на что-то кроме 1.
Носредна
124
Лично я не большой поклонник Крокфорда. Кажется, он считает все, что когда-либо вызывало ошибку в его коде, злым.
Паоло Бергантино
38
В JavaScript вы должны рассматривать каждую ошибку как нечто злое, поскольку нет официальной документации, нет поставщика сертификатов и вы не изучаете JS должным образом в университете. Крокфорд и Файрбаг заполнили эти пробелы в образовании JavaScript.
Stevek
40
++не вызывает ошибок. Использование ++«хитрых» способов может привести к ошибкам, особенно если кодовую базу обслуживает несколько человек, но это не проблема для оператора, это проблема для программиста. Я не изучал JS в университете (потому что он еще не существовал), но что с того? Я сделал C, который, конечно, был ++первым, но он также получает "и что?" Я не учился в университете, чтобы изучать конкретный язык, я пошел изучать хорошие практики программирования, которые я могу применить к любому языку.
nnnnnn

Ответы:

408

На мой взгляд, всегда использовать ++ и - сами по себе в одной строке, как в:

i++;
array[i] = foo;

вместо

array[++i] = foo;

Все, что за этим, может запутать некоторых программистов, и, на мой взгляд, оно того не стоит. Циклы for являются исключением, поскольку использование оператора приращения идиоматично и, следовательно, всегда понятно.

cdmckay
источник
3
Да, я тоже так делаю. Трудно ошибиться, и другим людям легче понять, что вы делаете.
Носредна
65
Это, кажется, становится ядром проблемы и моей путаницы. Используется один, i ++; кристально чистый. В ту минуту, когда вы добавляете другой код вокруг этого базового выражения, читаемость и ясность начинают страдать.
artlung
2
Согласитесь, но это связано не только с JavaScript
Тобиас
5
Вы не говорите, пишете ли вы на C или C ++. Возможно, вы должны использовать префиксный оператор приращения в C ++, в случае, если iпеременная впоследствии станет составным классом, и в этом случае вы будете без необходимости создавать временный объект. Я считаю, что оператор postfix более эстетичен.
mc0e
2
Я предпочитаю 1-строчную версию. Это напоминает работу стека. push это массив [++ i] = foo, pop это bar = массив [i--]. (Но с массивами, основанными на 0, array [i ++] = foo и bar = array [- i] выглядят еще лучше). Другое дело, я бы не хотел, чтобы кто-то добавил дополнительный код между i ++; и массив [i] = foo ;.
Флориан Ф
257

Я откровенно смущен этим советом. Часть меня задается вопросом, связано ли это с недостатком опыта (воспринимаемого или действительного) с кодировщиками JavaScript.

Я вижу, как кто-то, просто «взломавший» какой-то пример кода, может совершить невинную ошибку с ++ и -, но я не понимаю, почему опытный профессионал мог бы их избежать.

Джон Б
источник
7
Хороший вопрос, Джон Б. Вот почему это меня так беспокоит. Крокфорд - профессиональный программист (на десятилетия вперед), много десятилетий владеющий несколькими языками и работающий с JavaScript в основном с момента его создания. Я не думаю, что его совет исходит от «я ленивый и невнимательный неопытный хакер» - вот почему я пытаюсь понять, откуда это исходит.
artlung
14
@artlung Может быть, это больше связано с "ленивым и невнимательным неопытным хакером", вероятно, возиться с этим кодом, и я не хочу его путать.
Крис Кадмор
9
Я полностью не согласен с вашим ответом. На мой взгляд, дело не в том, достаточно ли я опытен, чтобы им пользоваться? - но "яснее или нет?" Если внутри цикла или в одиночку, это ясно, что все поймут, без вреда. Проблема возникает, когда ++выражение в более запутанном выражении, в котором ++ отличается от x ++, приводит к чему-то непростому для чтения. Идея Крокфорда не в том, могу ли я это сделать? это о том, как я могу избежать ошибок?
ArtoAle
1
Это личное предпочтение, в какой момент ясность важнее для вас, чем компактность. Это выходит за рамки опыта, достаточного для понимания кода. Смотрите другие ответы для примеров, где это больше проблем, чем стоит.
Фрукт
1
Я абсолютно согласен. Зачем гандикапить себя, не используя часть языка? Я нахожу веские причины для использования как пост-, так и пре-приращений и уменьшений. Для меня такая строка понятна и лаконична ... if (--imagesLoading === 0) start (); Если вы думаете, что ваш код неясен, используйте комментарии!
xtempore
69

В С есть история таких вещей:

while (*a++ = *b++);

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

И всегда есть вопрос о том, что

++i = i++;

или

i = i++ + ++i;

на самом деле Это определено в некоторых языках, а в других нет гарантии, что произойдет.

Помимо этих примеров, я не думаю, что есть что-то более идиоматическое, чем цикл for, который использует ++приращение. В некоторых случаях вы можете избежать цикла foreach или цикла while, который проверяет другое условие. Но искажать код, чтобы попытаться избежать приращения, смешно.

Затмение
источник
104
я = я ++ + ++ я; заставил мои глаза немного болеть - :)
Hugoware
3
Мой тоже. Даже если бы они не были одной и той же переменной, скажем, у меня был x = a ++ + ++ b; - эта комбинация мгновенно делает х, а и б сложнее держать в моей голове. теперь, если бы каждый был отдельным оператором в отдельных строках, как в - a ++; ++ б; х = а + б; было бы легче понять с первого взгляда.
artlung
10
Увы, (a ++; b ++; x = a = b) не совпадает со значением (a ++ + ++ b).
Thomas L Holaday
8
@Marius: x = a+++b-> x = (a++)+b-> x = a + b; a++. Токенизатор жадный.
Каллум Роджерс
14
Для дополнительной безопасности работы удалите пробелы в последнем примере.
mc0e
48

Если вы прочитаете JavaScript The Good Parts, вы увидите, что замена Крокфорда для i ++ в цикле for - это i + = 1 (не i = i + 1). Это довольно чисто и читабельно, и с меньшей вероятностью превратится во что-то «хитрое».

Крокфорд сделал запрещая автоинкрементный и autodecrement в опции в JSLint. Вы выбираете, следовать ли совету или нет.

Мое личное правило - не делать ничего в сочетании с автоинкрементом или автообращением.

Из многолетнего опыта в Си я узнал, что я не получаю переполнения буфера (или индекса массива за пределами границ), если я буду использовать его просто. Но я обнаружил, что переполнение буфера действительно происходит, если я впадаю в «чрезмерно хитрую» практику выполнения других вещей в одном и том же утверждении.

Так что, для моих собственных правил, использование i ++ в качестве приращения в цикле for хорошо.

Nosredna
источник
Отличное замечание о том, что опция «plusplus» - это просто опция. Основываясь на ваших и других ответах, я думаю, что я понимаю, что ++ сам по себе или цикл for сами по себе не сбивают с толку. Как только вы объедините этот ++ в другое утверждение, ваше заявление станет более сложным для чтения и понимания в контексте.
artlung
6
каждый получает переполнение буфера. Даже OpenSSH с их открытым исходным кодом и многочисленными редакциями
Eric
11
@Eric Пересматривая ваш пятилетний комментарий: о, как вы правы!
wchargin
27

В цикле это безвредно, но в операторе присваивания это может привести к неожиданным результатам:

var x = 5;
var y = x++; // y is now 5 and x is 6
var z = ++x; // z is now 7 and x is 7

Пробел между переменной и оператором также может привести к неожиданным результатам:

a = b = c = 1; a ++ ; b -- ; c; console.log('a:', a, 'b:', b, 'c:', c)

В заключение неожиданные результаты также могут быть проблемой:

var foobar = function(i){var count = count || i; return function(){return count++;}}

baz = foobar(1);
baz(); //1
baz(); //2


var alphabeta = function(i){var count = count || i; return function(){return ++count;}}

omega = alphabeta(1);
omega(); //2
omega(); //3

И это вызывает автоматическую вставку точки с запятой после новой строки:

var foo = 1, bar = 2, baz = 3, alpha = 4, beta = 5, delta = alpha
++beta; //delta is 4, alpha is 4, beta is 6

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

Ссылки

Пол Свит
источник
13
Но они не являются «неожиданными», если вы понимаете оператора. Это как никогда не использовать, ==потому что вы не понимаете разницу между ==и ===.
greg84
1
Точно. Есть вопрос C #, который опровергает ложные предположения.
Пол Свитт
Я думаю, что точка зрения Крокфорда состоит в том, что большинство программистов не полностью «понимают оператора», даже когда они думают, что понимают; следовательно, существует опасность их использования, потому что вероятность недопонимания высока. Смотрите @ комментарий Пола о ложных предположениях, который подкрепляет аргумент, что люди обычно неправильно понимают, как на самом деле работают префиксные и постфиксные операторы. Тем не менее, я признаю, что мне нравится их использовать, но ради других я стараюсь ограничить их использование идиоматическими случаями (см . Ответ cdmckay ).
Gfullam
Ваша ссылка на пробелы, похоже, не работает.
Руслан
Что такого сложного в вашем "пробельном" примере? Я получаю прямой вывод a: 2 b: 0 c: 1. Я не вижу ничего странного или неожиданного в первом примере («оператор присваивания»).
Кен Уильямс
17

Рассмотрим следующий код

    int a[10];
    a[0] = 0;
    a[1] = 0;
    a[2] = 0;
    a[3] = 0;
    int i = 0;
    a[i++] = i++;
    a[i++] = i++;
    a[i++] = i++;

так как i ++ оценивается дважды, вывод получен (из отладчика vs2005)

    [0] 0   int
    [1] 0   int
    [2] 2   int
    [3] 0   int
    [4] 4   int

Теперь рассмотрим следующий код:

    int a[10];
    a[0] = 0;
    a[1] = 0;
    a[2] = 0;
    a[3] = 0;
    int i = 0;
    a[++i] = ++i;
    a[++i] = ++i;
    a[++i] = ++i;

Обратите внимание, что вывод одинаков. Теперь вы можете подумать, что ++ i и i ++ - это одно и то же. Они не

    [0] 0   int
    [1] 0   int
    [2] 2   int
    [3] 0   int
    [4] 4   int

Наконец, рассмотрим этот код

    int a[10];
    a[0] = 0;
    a[1] = 0;
    a[2] = 0;
    a[3] = 0;
    int i = 0;
    a[++i] = i++;
    a[++i] = i++;
    a[++i] = i++;

Выход сейчас:

    [0] 0   int
    [1] 1   int
    [2] 0   int
    [3] 3   int
    [4] 0   int
    [5] 5   int

Так что они не одинаковы, смешивание обоих приводит к не столь интуитивному поведению. Я думаю, что для циклов нормально с ++, но будьте осторожны, когда у вас есть несколько символов ++ в одной строке или той же инструкции

Эрик
источник
3
Спасибо за проведение этого эксперимента. Особенно "простой на вид" код, но моему мозгу трудно удержать его очень быстро. Определенно попадает в категорию "хитрый".
artlung
28
Я остановился после первого примера. Вы сказали: «Рассмотрим следующий код». Нет, никто не пишет этот код. Это тот идиот, который пишет, что у него недостатки, а не языковая конструкция.
Сэм Харвелл
4
Вы показали, что два разных куска кода дают разные результаты, и это доказывает, что это плохо?
Ник
7
вывод таков: «Остерегайтесь, когда у вас несколько символов ++ в одной строке или одной и той же инструкции». Я не думаю, что вы читали это
Эрик
1
«поскольку i ++ оценивается дважды» - неправильно! Двойное изменение переменной между точками последовательности, а также ее использование недопустимо в C ++ и приводит к неопределенному поведению. Таким образом, компилятор может делать все, что захочет. Представленные здесь результаты являются случайными для реализации компилятора.
tbleher
16

Природа «до» и «после» операторов приращения и убывания может сбивать с толку тех, кто с ними не знаком; это один из способов, которым они могут быть хитрыми.

Пол Сонье
источник
29
Согласовано; Однако, опытный профессионал должен не следует путать.
Nate
1
Я думаю, что это похоже на руководство, чтобы не делать все статичным или глобальным. В этом нет ничего плохого, как в программной конструкции, но неумышленное чрезмерное использование может привести к плохому коду.
Джоэл Мартинес
6
Вопрос не в том, сможет ли хороший программист «пробиться» через логику - в идеале хороший код не требует никакой работы для понимания. Я думаю, что идея McWafflestix состоит в том, что вы не должны писать код, который после краткого осмотра может привести к добавлению ошибок через две недели или два года. Я знаю, что был виновен в добавлении кода в процедуру, которую я до конца не понимал :)
Майк
1
@Mike: да, обратите внимание, как я не указал, могут ли операторы быть хитрыми для первоначального автора кода или для более поздних сопровождающих. Как правило, мы пытаемся предположить, что оригинальные кодеры не используют конструкции, с которыми они не знакомы / не знакомы (к сожалению, это часто неверное предположение); однако это знакомство, конечно, не распространяется на сопровождающих (и, как указано в скобках выше, может даже не распространяться на оригинального автора).
Пол Сонье
Я пытаюсь написать свой код для чтения другими программистами, но я не пытаюсь писать его для чтения новичками.
Люк Н
16

С моей точки зрения, «явное всегда лучше, чем неявное». Потому что в какой-то момент вы можете запутаться с этим оператором приращений y+ = x++ + ++y. Хороший программист всегда делает свой код более читабельным.

Sriram
источник
1
В x ++ или ++ y нет ничего неявного.
Флориан Ф
1
Этот читаемый код сложен ... Я думаю, что существует базовая черта того, насколько простым должен быть ваш код, и мы, как сообщество, должны немного развиваться и иметь возможность читать то, что сейчас не читаемо, например, однострочные с вложенными троицами, чтобы позволить больше вещей, чтобы пнуть.
Хамза Уагад
14

Наиболее важным обоснованием для избежания ++ или - является то, что операторы возвращают значения и вызывают побочные эффекты в одно и то же время, что усложняет анализ кода.

Ради эффективности я предпочитаю:

  • ++ я когда не использую возвращаемое значение (не временно)
  • i ++ при использовании возвращаемого значения (без остановки конвейера)

Я фанат мистера Крокфорда, но в этом случае я не согласен. ++iна 25% меньше текста для разбора, чем i+=1 и, возможно, яснее.

Ханс Малербе
источник
13

Я смотрел видео Дугласа Крокфорда об этом, и его объяснение того, что не используется приращение и уменьшение, состоит в том, что

  1. Это использовалось в прошлом на других языках, чтобы выйти за границы массивов и вызвать все манеры зла и
  2. Что это больше сбивает с толку и неопытные разработчики JS не знают точно, что он делает.

Во-первых, массивы в JavaScript имеют динамический размер, поэтому, извините, если я ошибаюсь, невозможно выйти за границы массива и получить доступ к данным, к которым не следует обращаться с помощью этого метода в JavaScript.

Во-вторых, должны ли мы избегать сложных вещей, конечно, проблема не в том, что у нас есть эта возможность, а в том, что есть разработчики, которые утверждают, что делают JavaScript, но не знают, как работают эти операторы? Это достаточно просто. значение ++, укажите текущее значение и после выражения добавьте к нему единицу, значение ++, увеличьте значение перед тем, как передать его.

Выражения, такие как +++ ++ b, просты для понимания, если вы просто помните выше.

var a = 1, b = 1, c;
c = a ++ + ++ b;
// c = 1 + 2 = 3; 
// a = 2 (equals two after the expression is finished);
// b = 2;

Я полагаю, вы просто должны вспомнить, кто должен читать код, если у вас есть команда, которая знает JS наизнанку, вам не нужно беспокоиться. Если нет, то прокомментируйте, напишите по-другому и т. Д. Делайте то, что вы должны делать. Я не думаю, что инкремент и декремент по своей сути плох, генерируют ошибки или создают уязвимости, возможно, просто менее читабельны, в зависимости от вашей аудитории.

Кстати, я думаю, что Дуглас Крокфорд в любом случае легенда, но я думаю, что он сильно напугал оператора, который этого не заслуживал.

Я живу, чтобы оказаться неправым, хотя ...

Стюарт Уэйкфилд
источник
10

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

function testIncrement1(x) {
    return x++;
}

function testIncrement2(x) {
    return ++x;
}

function testIncrement3(x) {
    return x += 1;
}

console.log(testIncrement1(0)); // 0
console.log(testIncrement2(0)); // 1
console.log(testIncrement3(0)); // 1

Как вы можете видеть, после оператора return не следует использовать постинкремент / декремент, если вы хотите, чтобы этот оператор влиял на результат. Но return не «ловит» операторы постинкрементного увеличения / уменьшения:

function closureIncrementTest() {
    var x = 0;

    function postIncrementX() {
        return x++;
    }

    var y = postIncrementX();

    console.log(x); // 1
}
Евгений Левин
источник
Только одна из ваших функций получает х в качестве параметра
Calimero100582
8

Я думаю, что программисты должны быть компетентными в языке, который они используют; используйте это ясно; и используйте это хорошо. Я не думаю, что они должны искусственно калечить язык, который они используют. Я говорю из опыта. Однажды я работал буквально по соседству с магазином Cobol, где они не использовали ELSE «потому что это было слишком сложно». Reductio ad absurdam.

Маркиз Лорн
источник
@ Downvoter Увлекательный. Вы не думаете, что программисты должны быть компетентными в языке? Вы думаете, что они должны искусственно калечить язык? Что он? Нет третьего выбора здесь.
Маркиз Лорн
1
и припев для этого примера COBOL, еще больше радует, что COBOL в основном вымер до того, как я начал заниматься кодированием
Марк К Коуэн
1
@MarkCowan Я не понимаю, почему. Моя точка зрения касается произвольного ограничения, а не языка. Анекдот не указывает на что-то не так с Коболом. То, что нужно было вымирать, это тот магазин программирования, и он это сделал. Посетите внутренности любого крупного банка, и вы обнаружите, что Кобол жив и здоров.
маркиз Лорн
2

Как упоминалось в некоторых существующих ответах (что, к сожалению, я не могу комментировать), проблема в том, что x ++ ++ x оценивает разные значения (до и после приращения), что неочевидно и может привести к путанице - если это значение используется. cdmckay предлагает весьма разумно разрешить использование оператора приращения, но только таким образом, чтобы возвращаемое значение не использовалось, например, в отдельной строке. Я бы также включил стандартное использование в цикл for (но только в третьем операторе, возвращаемое значение которого не используется). Я не могу придумать другой пример. Будучи «сожженным» сам, я бы порекомендовал такое же руководство и для других языков.

Я не согласен с утверждением, что эта чрезмерная строгость вызвана неопытностью многих программистов на JS. Это именно тот тип письма, который типичен для «чрезмерно умных» программистов, и я уверен, что он гораздо чаще встречается на более традиционных языках и у разработчиков JS, имеющих опыт работы с такими языками.

Рядом с Приманом
источник
1
Спасибо за ответ. Для комментирования вам понадобится репутация 50 или выше. Смотрите: stackoverflow.com/faq
artlung
1
@artlung: я знаю об этом и об обосновании этого. Я просто говорил, что это раздражает :)
Рядом с Приманом
2

По моему опыту, ++ i или i ++ никогда не вызывали путаницы, кроме как при первом знакомстве с работой оператора. Это важно для самых базовых циклов и циклов while, которые преподаются в любой школе или колледже на языках, где вы можете использовать оператор. Лично я считаю, что делать что-то наподобие того, что написано ниже, лучше и читать, чем что-то, когда ++ находится на отдельной строке.

while ( a < 10 ){
    array[a++] = val
}

В конце концов, это предпочтение стиля, а не что-то более, что более важно, что когда вы делаете это в своем коде, вы остаетесь последовательными, так что другие, работающие над одним и тем же кодом, могут следовать и не должны обрабатывать одну и ту же функциональность разными способами. ,

Кроме того, Крокфорд, кажется, использует i- = 1, что, на мой взгляд, труднее читать, чем --i или i--

burdz
источник
2

Мой 2cents заключается в том, что их следует избегать в двух случаях:

1) Если у вас есть переменная, которая используется во многих строках, и вы увеличиваете / уменьшаете ее в первом операторе, который ее использует (или последний, или, что еще хуже, в середине):

// It's Java, but applies to Js too
vi = list.get ( ++i );
vi1 = list.get ( i + 1 )
out.println ( "Processing values: " + vi + ", " + vi1 )
if ( i < list.size () - 1 ) ...

В подобных примерах вы можете легко пропустить, что переменная автоматически увеличивается / уменьшается, или даже удалить первый оператор. Другими словами, используйте его только в очень коротких блоках или там, где переменная появляется в блоке только на паре операторов close.

2) В случае нескольких ++ и - примерно одной переменной в одном выражении. Очень сложно вспомнить, что происходит в таких случаях:

result = ( ++x - --x ) * x++;

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

zakmck
источник
1

Является ли Фортран С-подобным языком? У него нет ни ++, ни -. Вот как вы пишете цикл :

     integer i, n, sum

      sum = 0
      do 10 i = 1, n
         sum = sum + i
         write(*,*) 'i =', i
         write(*,*) 'sum =', sum
  10  continue

Элемент индекса i увеличивается по правилам языка каждый раз в цикле. Если вы хотите увеличить на что-то, отличное от 1, считать в обратном порядке на два, например, синтаксис ...

      integer i

      do 20 i = 10, 1, -2
         write(*,*) 'i =', i
  20  continue

Является ли Python C-подобным? Он использует определения диапазона и списка и другие синтаксисы, чтобы обойти необходимость увеличения индекса:

print range(10,1,-2) # prints [10,8.6.4.2]
[x*x for x in range(1,10)] # returns [1,4,9,16 ... ]

Таким образом, основываясь на этом элементарном исследовании ровно двух альтернатив, разработчики языка могут избежать ++ и - предвидя варианты использования и предоставляя альтернативный синтаксис.

Являются ли Фортран и Питон заметно менее сильной ошибкой, чем процедурные языки с ++ и -? У меня нет доказательств.

Я утверждаю, что Фортран и Питон похожи на С, потому что я никогда не встречал человека, свободно владеющего Си, который не мог бы с 90% точностью угадать намерение не скрытого Фортрана или Питона.

Thomas L Holaday
источник
1
Да, IFortran из 1950-х, в то время как C из 1970-х, так что, возможно, Фортрану нет C-подобного. Интересен пример Python, в котором отсутствует что-то вроде оператора plusplus. Итерации по структурам данных более просты в Python, поскольку Python является «по-другому» объектно-ориентированным JavaScript. Возможно, совет Крокфорда заключается в использовании большего количества ОО-подобного синтаксиса для итерации.
artlung