许多运算符在我们上学的时候就学到了。包括加 +,减 -,乘 * 等。

本章我们将关注的 JavaScript 中的运算符——包括那些没有在学校学到的。

术语:“一元”,“二元”和“操作数”

开始讲之前,需要先掌握一些通用的术语。

  • 操作数 表示运算符应用的对象。例如,5 * 2 这个表达式中有两个操作数:左边的操作数是 5,右边的操作数是 2。有时我们还叫“参数”。
  • 一元运算符 表示与一个操作数配合使用的运算符。例如,比如一元负操作符可以用来反转数值的正负:
  1. let x = 1;
  2. x = -x;
  3. alert( x ); // -1, 使用了一元减运算符
  • 两元运算符 表示有两个操作数。减号也就包括两元的使用形式:
  1. let x = 1, y = 3;
  2. alert( y - x ); // 2, 两元减号用来求两个值的差

形式上,我们讨论的是两个不同的运算符:一元负操作符(单操作数,反转正负)和两元减法操作符(两个操作数,相减)。

字符串连接,两元 +

现在让我们来看看JavaScript操作符的特殊特性,这些特性超出了学校所学的算术运算范围。

通常加运算符 + 用来相加两个数值。

但是如果是用在字符串上的时候,然后另一个也被转换成一个字符串。

例如:

  1. alert( '1' + 2 ); // "12"
  2. alert( 2 + '1' ); // "21"

看,具体是第一个操作数是字符串还是第二个操作数无关紧要。规则很简单:如果任一操作数是字符串,那么将另一个操作数也转换成字符串。

但是,请注意,操作是从左到右运行的。如果有两个数字后跟一个字符串,那么在转换成字符串之前,这些数字将先相加:

  1. alert(2 + 2 + '1' ); // 结果是 "41" 而不是 "221"

字符串连接和转换是两元 + 操作符的一个特殊特性。其他的算术运算符只适用于数字,他们总是将操作数转换成数值。

例如,减和除:

  1. alert( 2 - '1' ); // 1
  2. alert( '6' / '2' ); // 3

数值转换,一元 +

  • 有两种存在形式。我们上面用的二元形式和下面要讲的一元形式。

一元加,或者说,应用于单个值的 + 运算符,不会对数值做出任何处理,但是如果操作数不是数值的话就会转换成数值。

例如:

  1. // No effect on numbers
  2. let x = 1;
  3. alert( +x ); // 1
  4. let y = -2;
  5. alert( +y ); // -2
  6. // Converts non-numbers
  7. alert( +true ); // 1
  8. alert( +"" ); // 0

它的作用跟 Number(…) 很像,但是写起来更短。

将字符串转换成数字的需求经常出现。例如,如果我们从HTML表单字段获取值,那么它们通常是字符串。

如果我们想把它们加起来呢?

两元加会将它们作为字符串添加:

  1. let apples = "2";
  2. let oranges = "3";
  3. alert( apples + oranges ); // "23", the binary plus concatenates strings

如果我们想把它们看成数字,那么我们可以转换,然后求和:

  1. let apples = "2";
  2. let oranges = "3";
  3. // both values converted to numbers before the binary plus
  4. alert( +apples + +oranges ); // 5
  5. // the longer variant
  6. // alert( Number(apples) + Number(oranges) ); // 5

从数学家的角度来看,大量的加号可能看起来很奇怪。但是从程序员的角度来看,没有什么特别的东西:一元的加号首先被应用,它们把字符串转换成数字,然后二元加加把它们加起来。

为什么一元加会比二元加运算符优先应用于值呢?正如我们将要看到的,这是因为它有更高的优先级

运算符优先级

如果一个表达式有不止一个运算符,那么执行顺序是由它们的优先级决定的,或者,换句话说,操作符中有一个隐式的优先级顺序。

从学校我们都知道表达式 1+2 * 2 中的乘法应该在加法之前计算出来。这是最优先的事情,乘法比加法有有更高的优先级。

括号大于任何优先级,所以如果我们对顺序不满意,我们可以使用它们,比如:(1 + 2) * 2。

JavaScript 中有许多操作符。每个操作符都有相应的优先级,一个有更大的数字的那个先执行。如果优先级是相同的,则执行顺序从左到右。

来自优先表的摘录(您不需要记住这一点,但是请注意,一元运算符比对应的二进制文件要高):

优先级 名称 符号.
16 一元加 +
16 一元减 -
14 *
14 /
13 +
13 -
3 赋值 =

我们知道,“一元加”的优先级是 16,比“加”(两元加)的优先级 13 要高。这就是为什么表达式 +apples + +oranges 中先运行一元加,其次是加法。

赋值

需要注意的是,这里的赋值 = 是一个运算符。它在优先级表中被列出,具有很低的优先级 3。

这就是为什么,当我们赋值给一个变量的时候,像 x = 2 * 2 + 1,首先进行计算,然后才使用 = 进行赋值,并将值存储在结果 x 中。

  1. let x = 2 * 2 + 1;
  2. alert( x ); // 5

同时我们还可以链式的赋值:

  1. let a, b, c;
  2. a = b = c = 2 + 2;
  3. alert( a ); // 4
  4. alert( b ); // 4
  5. alert( c ); // 4

链式赋值从右到左分配。首先最右边的表达式 2 + 2 计算然后将值赋给左边的变量:c、b 和 a。最后,所有的变量共享同一个值。

⚠️赋值运算符 = 会返回一个值

运算符总是会返回一个值。这对于大多数运算符来说很明显,比如加 + 或者乘 *。但是赋值运算符也遵循这个规则。

x = value 将 value 的值写入 value,然后返回它。

这是一个 demo,它使用一个赋值作为一个更复杂的表达式的一部分:

上面例子里,(a = b + 1) 的结果是赋值给变量 a 的值(就是 3),然后再拿 3 减它。

有趣的代码,不是吗?我们应该理解它是如何工作的,因为有时我们可以在第三方库中看到它,但是我们不应该自己写这样的东西。这样的技巧绝对不能使代码更清晰、更可读。

  1. let a = 1;
  2. let b = 2;
  3. let c = 3 - (a = b + 1);
  4. alert( a ); // 3
  5. alert( c ); // 0

取模 %

余数运算符 % 尽管看起来好像有,但与百分比没有关系。

a % b 的结果是 a 被 b 除后,得到的余数。

例如:

  1. alert( 5 % 2 ); // 5 除以 2 得到的余数是 1
  2. alert( 8 % 3 ); // 8 除以 3 得到的余数是 2
  3. alert( 6 % 3 ); // 8 除以 3 得到的余数是 0

指数运算符 **

指数运算符 ** 是最近才加入到语言中的。

对于一个自然数 b,a ** b 的结果是 a 与自身相乘了 b 次。

例如:

  1. alert( 2 ** 2 ); // 4 (2 * 2)
  2. alert( 2 ** 3 ); // 8 (2 * 2 * 2)
  3. alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2)

这个操作符适用于 a 和 b 是非整数的情况:

例如:

  1. alert( 4 ** (1/2) ); // 2 (power of 1/2 is the same as a square root, that's maths)
  2. alert( 8 ** (1/3) ); // 2 (power of 1/3 is the same as a cubic root)

自增/自减

自增或自减一个数值是最常见的数学运算之一。

因此,有一些特殊的运算符:

  • 自增 ++ 将变量的值加 1:
  1. let counter = 2;
  2. counter++; // works the same as counter = counter + 1, but is shorter
  3. alert( counter ); // 3
  • 自减 — 将变量的值减 1:
  1. let counter = 2;
  2. counter--; // works the same as counter = counter - 1, but is shorter
  3. alert( counter ); // 1

⚠️重要:

自增/自减仅对变量有效。在一个值上使用,比如 5++ 会产生报错。

运算符 ++ 和 — 既可以用在一个变量的前面,也可以用在后面:

  • 当放置于一个变量之后的时候,它被称为“后缀形式”:counter++。
  • “前缀形式”是指运算符放在变量前面:++counter。

这两种形式的记录是一样的结果:将 counter 的值加 1。

但是有区别吗?有的,只有在观察 ++/— 返回的值的时候就能看出来了。

让我们澄清。如我们所知,所有的操作符都返回一个值。在这里,自增/自减也不例外。前缀形式返回新值,而后缀形式返回旧值(相较于自增/自减之前)。

为了看到区别,这里有一个例子:

  1. let counter = 1;
  2. let a = ++counter; // (*)
  3. alert(a); // 2

在 (*) 处,前缀调用 ++counter 先增加 counter 的值,然后返回新值 2。因此会 alert 出来 2。

现在让我们使用后缀形式:

  1. let counter = 1;
  2. let a = counter++; // (*) 将 ++counter 改变为 counter++
  3. alert(a); // 1

在 (*) 处,后缀调用 counter++ 依旧会增加 counter 的值,但是返回的是值(相较于增加前)。因此 alert 出来是 1。

总结一下:

  • 如果不使用自增/自减运算符返回值,那么使用两种形式没有区别:
  1. let counter = 0;
  2. counter++;
  3. ++counter;
  4. alert( counter ); // 2, the lines above did the same
  • 如果我们想要增加这个值并要立即使用运算符的结果,就需要前缀形式:
  1. let counter = 0;
  2. alert( ++counter ); // 1
  • 如果我们想要增加,但是使用之前的值,那么我们需要后缀形式:
  1. let counter = 0;
  2. alert( counter++ ); // 0

⚠️在其他运算符中间的自增/自减运算符

运算符 ++/— 也可以在表达式里使用,它们的优先级高于大多数其他算术运算。

例如:

  1. let counter = 1;
  2. alert( 2 * ++counter ); // 4

相比较:

  1. let counter = 1;
  2. alert( 2 * counter++ ); // 2, 因为 counter++ 返回的是 "旧" 值

尽管在技术上是允许的,这样的符号通常会使代码的可读性降低。一行里做很多事情——不好。

在阅读代码时,快速的“垂直”眼球扫描很容易就会错过 counter++,而且变量的增加也不明显。

建议使用“一行——一个行为”的风格:

  1. let counter = 1;
  2. alert( 2 * counter );
  3. counter++;

位操作符

位运算符将参数视为 32 位整数,并在二进制表示的级别上工作。

这些操作符不是特定于 JavaScript 的,它们在大多数编程语言中都得到了支持。

运算符列表:

  • 与(&)
  • 或(|)
  • XOR ( ^ )
  • NOT ( ~ )
  • LEFT SHIFT ( << )
  • RIGHT SHIFT ( >> )
  • ZERO-FILL RIGHT SHIFT ( >>> )

这些操作符很少使用。为了理解它们,我们应该深入研究底层的数字表示,现在就不可能做到这一点。特别是因为我们很快就不需要它们了。如果您感兴趣,您可以阅读 MDN 中的位运算符文章。当真正需要的时候,这样做会更实际一些。

原地修改

我们经常需要将一个运算符应用到一个变量中并将新的结果存储在其中

例如:

  1. let n = 2;
  2. n = n + 5;
  3. n = n * 2;

这种符号可以简写为 += 和 *=。

  1. let n = 2;
  2. n += 5; // now n = 7 (same as n = n + 5)
  3. n *= 2; // now n = 14 (same as n = n * 2)
  4. alert( n ); // 14

对于所有算术和位运算符,都存在简短的“修改和赋值”操作符:/=、-= 等。

这些操作符具有与正常赋值运算符有一样的优先级,因此它们在大多数其他计算之后运行:

  1. let n = 2;
  2. n *= 3 + 5;
  3. alert( n ); // 16 (right part evaluated first, same as n *= 8)

逗号

逗号操作符 , 是最少见和不寻常的操作符之一。有时它被用来编写更短的代码,所以我们需要知道它,以便理解发生了什么。

逗号操作符允许我们对几个表达式求值,用逗号 , 分隔。每一个表达式都会被计算,但是只有最后一个的结果返回。

例如:

  1. let a = (1 + 2, 3 + 4);
  2. alert( a ); // 7 (the result of 3 + 4)

在这里,第一个表达式 1+2 被计算,它的结果被丢弃,然后 3+4 的计算结果返回。

⚠️逗号运算符的优先级很低

需要注意的是,逗号运算符的优先级很低,比 = 还低,所以括号在上面的例子中很重要。

没有括号的话:a = 1 + 2, 3 + 4 首先计算 + 号,计算和 a = 3, 7,然后赋值运算符 = 赋值 a = 3,然后数值之后的 7 不会被处理,会被忽略。

为什么我们需要这样一个运算符,除了最后一部分之外,什么都扔掉?
有时人们在更复杂的结构中使用它,在一行中放入多个操作。

例如:

  1. // three operations in one line
  2. for (a = 1, b = 3, c = a * b; a < 10; a++) {
  3. ...
  4. }

在许多 JavaScript 框架中都使用了这样的技巧,这就是我们提到它们的原因。但通常它们不会提高代码的可读性,所以在这样写之前我们应该好好考虑一下。

(完)


📄 文档信息

🕘 更新时间:2021/09/23
🔗 原文链接:https://javascript.info/operators