ChatGPT解决这个技术问题 Extra ChatGPT

为什么要避免 JavaScript 中的递增(“++”)和递减(“--”)运算符?

tips for jslint tool 之一是:

++ 和 -- 众所周知,++(递增)和 --(递减)运算符会通过鼓励过多的技巧来导致糟糕的代码。它们在启用病毒和其他安全威胁方面仅次于错误的架构。有一个 plusplus 选项禁止使用这些运算符。

我知道像 $foo[$bar++] 这样的 PHP 结构很容易导致一个错误,但我想不出比以下更好的方法来控制循环:

while( a < 10 ) do { /* foo */ a++; }

或者

for (var i=0; i<10; i++) { /* foo */ }

jslint 突出显示它们是因为有些类似的语言缺少“++”和“--”语法或处理方式不同,还是有其他理由避免使用“++”和“-- “我可能会失踪?

所以应该做 array[index=index+1] 而不是 array[++index] (如果甚至允许前者!)。真是麻烦。
我没见过 Crockford 做 index=index+1。我见过他做index+=1。我认为这是一个合理的选择。当您想将步幅更改为 1 以外的值时,这非常有用。
就个人而言,我不是 Crockford 的忠实粉丝。他似乎认为任何在他的代码中导致错误的东西都是邪恶的。
在 JavaScript 中,你应该认为每个 bug 都有些邪恶,因为没有官方文档,也没有证书提供者,你也没有在大学里正确地学习 JS。 Crockford 和 Firebug 填补了 JavaScript 教育中的这些漏洞。
++ 不会导致错误。以“棘手”的方式使用 ++可能 会导致错误,尤其是在不止一个人维护代码库的情况下,但这不是操作员的问题,而是程序员的问题。我在大学没有学过 JS(因为当时还不存在),但那又怎样?我确实做了 C,当然首先有 ++,但这也得到了“那又怎样?”我上大学不是为了学习一门特定的语言,我是去学习可以应用于任何语言的良好编程实践。

R
Rohit Sharma

我的观点是始终在一行中单独使用 ++ 和 -- ,例如:

i++;
array[i] = foo;

代替

array[++i] = foo;

除此之外的任何事情都会让一些程序员感到困惑,而且在我看来是不值得的。 For 循环是一个例外,因为增量运算符的使用是惯用的,因此总是很清楚。


是的,我也是这样做的。很难出错,而且其他人更容易弄清楚你在做什么。
这似乎是问题的核心和我的困惑。单独使用,i++;晶莹剔透。在您围绕该基本表达式添加其他代码的那一刻,可读性和清晰度开始受到影响。
同意,但它不仅与 JavaScript 相关
你没有说你是在写 C 还是 C++。可以说,您应该在 C++ 中使用前缀自增运算符,以防 i 变量后来成为复合类,在这种情况下,您将不必要地创建一个临时对象。不过,我发现后缀运算符在美学上更令人愉悦。
我更喜欢 1 行版本。它让人想起堆栈操作。 push 是 array[++i] = foo,pop 是 bar = array[i--]。 (但是对于基于 0 的数组,array[i++] = foo 和 bar = array[--i] 看起来更好)。另一件事,如果有人在 i++ 之间添加额外的代码,我会讨厌它;和数组[i] = foo;。
V
Vikrant

坦率地说,我对这个建议感到困惑。我的一部分想知道这是否与 javascript 编码人员缺乏经验(感知或实际)有关。

我可以看到有人只是“破解”一些示例代码可能会在使用 ++ 和 -- 时犯下一个无辜的错误,但我不明白为什么有经验的专业人士会避免它们。


好点 Jon B。这就是为什么它让我如此困扰。 Crockford 是一名职业程序员(价值数十年),数十年来精通多种语言,并且从 JavaScript 一开始就基本上使用它。我不认为他的建议来自“我是懒惰和不专心的缺乏经验的黑客”——这就是为什么我试图了解它的来源。
@artlung 也许它更多地与“一个懒惰和不专心的缺乏经验的黑客”有关,可能会弄乱这段代码,我不想让他感到困惑。
我完全不同意你的回答。在我看来,这不是关于“我有足够的经验来使用它吗?” - 但“是否更清楚?”如果在 for 循环中,或者单独使用,这很清楚 - 每个人都会理解这一点而不会造成伤害。当 ++x 与 x++ 不同时,将++放在更复杂的表达式中时,问题就出现了,从而导致某些内容不易阅读。 Crockford 的想法不是关于“我能做到吗?”它是关于“我怎样才能避免错误?”
个人偏好在哪一点上清晰比紧凑更重要。这超出了足够的经验来理解代码。有关麻烦大于价值的示例,请参见其他答案。
我完全同意。为什么不使用部分语言来妨碍自己?我找到了使用前后增量和减量的充分理由。对我来说,这样的一行是清晰而简洁的...... if (--imagesLoading === 0) start();如果您认为您的代码不清楚,请使用注释!
E
Eclipse

在 C 中有这样的历史:

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

复制一个字符串,也许这就是他所指的过度欺骗的根源。

总是有什么问题

++i = i++;

或者

i = i++ + ++i;

实际上做。它在某些语言中定义,而在其他语言中则无法保证会发生什么。

除了这些例子,我认为没有比使用 ++ 递增的 for 循环更惯用的了。在某些情况下,您可以使用 foreach 循环或检查不同条件的 while 循环。但是扭曲你的代码来尝试避免使用递增是荒谬的。


i = i++ + ++i;让我的眼睛有点痛-:)
我也是。即使它们不是同一个变量,假设我有 x = a++ + ++b; ——这种组合立即让我更难以记住 x、a 和 b。现在,如果每个语句都是在不同的行上的单独语句,如 --a++; ++b; x = a + b;乍一看会更容易理解。
唉,(a++; b++; x=a=b) 与 (a++ + ++b) 的值不同。
@Marius:x = a+++b --> x = (a++)+b --> x = a + b; a++。标记器是贪婪的。
为了额外的工作安全,请删除上一个示例中的空格。
N
Nosredna

如果您阅读 JavaScript The Good Parts,您会发现 Crockford 在 for 循环中对 i++ 的替换是 i+=1(而不是 i=i+1)。这是非常干净和可读的,并且不太可能变成“棘手”的东西。

Crockford 在 jsLint 中将禁止自动增量和自动减量作为一个选项。您可以选择是否遵循建议。

我自己的个人规则是不要做任何与自动增量或自动减量相结合的事情。

我从多年的 C 语言经验中了解到,如果我继续简单地使用它,就不会出现缓冲区溢出(或数组索引越界)。但是我发现如果我陷入在同一个语句中做其他事情的“过于棘手”的做法,我确实会遇到缓冲区溢出。

因此,根据我自己的规则,使用 i++ 作为 for 循环中的增量是可以的。


关于“plusplus”选项的优点就是这样,一个选项。根据您的回答和其他答案,我认为我感觉到 ++ 本身或 for 循环本身并不令人困惑。第二次您将该 ++ 组合成另一个语句,您的语句在上下文中变得更难以阅读和理解。
每个人都会缓冲区溢出。甚至 OpenSSH 也有他们的开源和他们的多个修订委员会
@Eric 重温你五年前的评论:哦,how right you were!
8
8 revs

在循环中它是无害的,但在赋值语句中它可能导致意想不到的结果:

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

preincrement/postincrement 混淆会产生非常难以诊断的错误。幸运的是,它们也完全没有必要。有更好的方法将 1 添加到变量中。

参考

JSLint 帮助:递增和递减运算符


但是,如果您了解运算符,它们就不会“出乎意料”。这就像从未使用过 ==,因为您不了解 ===== 之间的区别。
确切地。有一个 C# question 可以揭穿错误的假设。
我认为 Crockford 的观点是大多数程序员并没有完全“理解操作符”,即使他们认为自己理解。因此,使用它们存在危险,因为误解的可能性很高。请参阅@Paul 关于错误假设的评论,该评论支持人们通常误解前缀和后缀运算符实际工作方式的论点。也就是说,我承认我喜欢使用它们,但为了其他人,我尝试将它们的使用限制在惯用情况下(参见 cdmckay's answer)。
您的空白链接似乎已损坏。
您的“空白”示例有什么棘手之处?我得到了 a: 2 b: 0 c: 1 的直接输出。在第一个示例(“赋值语句”)中,我也没有看到任何奇怪或意外的东西。
E
Eric

考虑以下代码

    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

所以它们是不一样的,将两者混合会导致不那么直观的行为。我认为 for 循环可以使用 ++,但是当您在同一行或同一指令上有多个 ++ 符号时要小心


谢谢你做那个实验。特别是“看起来很简单”的代码,但我的大脑很难很快掌握。绝对属于“棘手”类别。
在第一个例子之后我停了下来。您说“考虑以下代码”。不,没有人编写该代码。有缺陷的是白痴写的,而不是语言结构。
您已经证明了两段不同的代码会产生不同的输出,这证明它很糟糕?
结论是“当您在同一行或同一指令上有多个 ++ 符号时要小心”。我想你没读过
埃里克,我看不出你想表达什么。该代码产生的结果正是人们所期望的。这里没有什么是违反直觉的。
P
Paul Sonier

递增和递减运算符的“前”和“后”性质可能会让不熟悉它们的人感到困惑;这是他们可能很棘手的一种方式。


同意;但是,经验丰富的专业人员不应混淆。
我认为这类似于避免使所有内容成为静态或全局的指导。作为一种编程结构,它本质上没有任何问题,但是无知的过度使用会导致糟糕的代码。
问题不在于一个好的程序员是否可以通过逻辑“按自己的方式工作”——好的代码理想情况下不需要任何工作来理解。我认为 McWafflestix 的观点只是你不应该编写经过简短检查可能会导致在两周或两年内添加错误的代码。我知道我一直在为我不完全理解的例程添加代码而感到内疚:)
@Mike:是的,请注意我没有指定操作符对于代码的原始作者或后来的维护者是否会很棘手。通常,我们尝试假设原始编码人员不使用他们不熟悉/不熟悉的结构(遗憾的是,这通常是一个错误的假设);但是,这种熟悉程度肯定不会扩展到维护者(并且,正如上面的括号中所指出的,甚至可能不会扩展到原始作者)。
我尝试编写代码供其他程序员阅读,但我不尝试编写代码供初学者阅读。
a
artlung

在我看来,“显式总是比隐式好。”因为在某些时候,您可能会对增量语句 y+ = x++ + ++y 感到困惑。一个好的程序员总是让他或她的代码更具可读性。


x++ 或 ++y 中没有任何隐含的内容。
这个可读的代码很棘手......我认为你的代码必须有多简单,我们 - 作为一个社区 - 必须茁壮成长并且能够阅读现在不可读的内容,例如单行使用嵌套的三元组,以允许更多的东西进入。
T
Tony

我一直在看 Douglas Crockford 的视频,他对不使用增量和减量的解释是

过去它在其他语言中被用来打破数组的界限并导致各种不良行为,而且它更令人困惑,没有经验的 JS 开发人员不知道它到底做了什么。

首先,JavaScript 中的数组是动态大小的,因此,如果我错了,请原谅我,不可能打破数组的边界并访问不应在 JavaScript 中使用此方法访问的数据。

其次,我们是否应该避免复杂的事情,当然问题不是我们有这个工具,而是问题是那里有开发人员声称做 JavaScript,但不知道这些运算符是如何工作的?这很简单。 value++,给我当前值,在表达式之后加一,++value,在给我之前增加值。

如果您只记得上面的内容,那么像 a ++ + ++ 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 了如指掌的团队,那么你就不用担心了。如果没有,请评论它,以不同的方式写它,等等。做你必须做的。我不认为增量和减量本质上是坏的或产生错误或产生漏洞,可能只是不太可读,具体取决于您的受众。

顺便说一句,我认为道格拉斯·克罗克福德无论如何都是一个传奇人物,但我认为他对一个不值得的操作员造成了很大的恐慌。

虽然我活着被证明是错误的......


H
Hans Malherbe

避免 ++ 或 -- 的最重要理由是运算符返回值并同时引起副作用,这使得对代码的推理变得更加困难。

为了效率,我更喜欢:

++i 不使用返回值时(非临时)

i++ 使用返回值时(无管道停顿)

我是克罗克福德先生的粉丝,但在这种情况下我不得不不同意。 ++ii+=1 少 25% 的文本解析,并且可以说更清晰。


i
ide

另一个例子,比其他一些简单的返回增量值更简单:

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
}

只有一个函数接收 x 作为参数
u
user207421

我认为程序员应该能够胜任他们使用的语言;清楚地使用它;并很好地使用它。我认为他们不应该人为地削弱他们使用的语言。我根据经验说话。我曾经在一家 Cobol 商店隔壁工作,他们没有使用 ELSE,因为它太复杂了。减少荒谬。


@downvoter 令人着迷。你不认为程序员应该精通这门语言吗?你认为他们应该人为地削弱语言吗?它是哪一个?这里没有第三选择。
以及对那个 COBOL 示例的支持,让我更加高兴的是,在我开始编码之前,COBOL 大部分都消失了
@MarkCowan 我不明白为什么。我的观点是关于任意限制,而不是语言。轶事并不表明 Cobol 有任何问题。需要消亡的是那个编程商店,它确实做到了。参观任何一家大型银行的内脏,你会发现 Cobol 活得很好。
b
burdz

以我的经验,++i 或 i++ 从未引起混淆,除非是在第一次了解运算符的工作原理时。对于任何高中或大学课程所教授的最基本的 for 循环和 while 循环来说,它都是必不可少的,这些课程以您可以使用运算符的语言教授。我个人发现做一些类似下面的事情看起来和阅读比 a++ 在单独的行上更好。

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

最后,这是一种风格偏好,仅此而已,更重要的是,当您在代码中执行此操作时,您会保持一致,以便其他使用相同代码的人可以遵循,而不必以不同的方式处理相同的功能.

此外,Crockford 似乎使用 i-=1,我发现它比 --i 或 i-- 更难阅读


N
Near Privman

正如在一些现有答案中提到的(令人讨厌的是我无法评论),问题是 x++ ++x 评估为不同的值(在增量之前与之后),这并不明显并且可能非常令人困惑 -如果使用该值。 cdmckay 非常明智地建议允许使用增量运算符,但只能以不使用返回值的方式,例如在其自己的行上。我还将在 for 循环中包含标准用法(但仅在第三个语句中,其返回值未使用)。我想不出另一个例子。我自己被“烧毁”了,我也会推荐其他语言的相同指南。

我不同意这种过于严格是由于许多 JS 程序员缺乏经验的说法。这正是“过于聪明”的程序员典型的写作类型,我相信它在更传统的语言和具有此类语言背景的 JS 开发人员中更为常见。


谢谢回复。您需要 50 或更高的声望值才能发表评论。请参阅:stackoverflow.com/faq
@artlung:我知道这一点,以及它背后的基本原理。我只是说这很烦人:)
z
zakmck

我的 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++;

考试和专业测试会询问上述示例,实际上我在寻找有关其中一个示例的文档时偶然发现了这个问题,但在现实生活中,不应该被迫对一行代码进行太多思考。


T
Thomas L Holaday

Fortran 是类 C 语言吗?它既没有 ++ 也没有 --。这是how you write a loop

     integer i, n, sum

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

每次循环时,索引元素 i 都会按语言规则递增。如果你想增加 1 以外的值,例如倒数 2,语法是 ...

      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 ... ]

因此,基于对两种替代方案的初步探索,语言设计者可以避免 ++ 和 -- 通过预测用例并提供替代语法。

与具有 ++ 和 -- 的程序语言相比,Fortran 和 Python 是否更容易吸引错误?我没有证据。

我声称 Fortran 和 Python 类似于 C,因为我从来没有遇到过不能以 90% 的准确率正确猜测非混淆 Fortran 或 Python 意图的人。


是的,IFortran 是 1950 年代的,而 C 是 1970 年代的,所以也许 Fortran 不是 C 语言。 Python 缺少 plusplus 运算符的例子很有趣。数据结构的迭代在 Python 中更简单地处理,Python 是“不同的”面向对象的 JavaScript。也许 Crockford 的建议是关于使用更多类似于 OO 的语法进行迭代。
H
Harrison Cramer

当用作前缀和后缀时,运算符的含义不同,这可能会导致难以发现的错误。考虑以下示例,使用冒泡排序:

function bubbleSort(array) {
  if(array.length === 1) return array;

  let end = array.length - 2;
  do {
    for (let i = 0; i < array.length; i += 1) {
      if (array[i] > array[i + 1]) {
        swap(array, i, i + 1);
      }
    }
  } while (end--);
}

bubbleSort([6,5]);

让我们想象在运行我们的程序的过程中,我们将一个包含两项的值传递给我们的排序函数。代码按原样运行良好:“do/while”循环在达到条件之前首先执行。但是,程序发现 end 是假的并在递减变量之前退出循环。

现在考虑以下代码,其中 -- 符号用作前缀,而不是后缀。此代码将进入无限循环:

function bubbleSort(array) {
  if(array.length === 1) return array;

  let end = array.length - 2;
  do {
    for (let i = 0; i < array.length; i += 1) {
      if (array[i] > array[i + 1]) {
        swap(array, i, i + 1);
      }
    }
  } while (--end);
}

bubbleSort([6,5]);

现在,当我们达到 while 条件时,我们在检查它之前减少结束值。这将返回 -1,它在 Javascript 中是一个真实值。

我对它们以一种或另一种方式使用没有强烈的意见,但我只是想展示它们如何在不小心使用时导致真正的错误。


t
teraryc kaspien

++ 的论点是 ++ 不适用于会导致 TypeError 的字符串,这几乎总是可取的。