一、运算元
运算元 —— 运算符应用的对象。比如说乘法运算 5 * 2,有两个运算元:左运算元 5 和右运算元 2。有时候人们也称其为“参数”而不是“运算元”。
- 如果一个运算符对应的只有一个运算元,那么它是 一元运算符;有两个则是 二元运算符;有三个是 三元运算符(也叫 三目运算符)。
二、算术运算符
1、运算规则
-
左右两侧都是
Number类型的时候- 左右为数值,进行正常的加减乘除取模运算(除数不能为0);
- 任何一侧为
NaN,则结果返回NaN; - 被除数是有限值,除数是无限值,则返回被除数:
console.log(2 % Infinity); // 2
// 除数不能为0 console.log(10 / 0); // Infinity console.log(0 / 0); // NaN // 取模时,除数为0,返回 NaN console.log(10 % 0); // NaN console.log(0 % 0); // NaN -
如果有一侧不是
Number类型的,则转为数字类型(Number())后,再进行计算。Number(null)为 0,Number(undefined)为NaN。
2、加减乘除取余求幂
- 加性操作符
- 加法
+ - 减法
-
- 加法
- 乘性操作符
- 乘法
* - 除法
/ - 取余(求模)
%
- 乘法
- 指数操作符
- 求幂
**,即Math.pow()
- 求幂
前四个都很简单,而 % 和 ** 则需要说一说。
2.1 取余 %
a % b 的结果是 a 整除 b 的 余数。
console.log( 5 % 2 ); // 1,5 除以 2 的余数
console.log( 8 % 3 ); // 2,8 除以 3 的余数
2.2 求幂 **
求幂运算 a ** b 是 a 乘以自身 b 次。
在数学上,求幂的定义也适用于非整数。
console.log( 4 ** (1/2) ); // 2(1/2 次方与平方根相同)
console.log( 8 ** (1/3) ); // 2(1/3 次方与立方根相同)
console.log(Math.pow(3, 2); // 9 (3的平方根)
console.log(3 ** 2); // 9 (3的平方根)
2.3 + 连接字符串(二元)
通常,加号 + 用于求和。但是如果加号 + 被应用于字符串,它将合并(连接)各个字符串:
let s = "my" + "string";
console.log(s); // mystring
console.log(2 + 2 + '1' ); // '41' 运算符按顺序工作。等同于 (2 + 2) + '1'
2.4 + 转化数字(一元)
如果运算元不是数字,加号 + 则会将其转化为数字。它的效果和 Number() 相同,但是更加简短。
let s1 = "01";
console.log(+s1); // 1
console.log( +true ); // 1
console.log( +"" ); // 0
2.5 自增自减(一元)
自增 ++ 将变量与 1 相加;自减 -- 将变量与 1 相减;
let counter = 2;
counter++; // 等同于 counter = counter + 1
counter--; // 等同于 counter = counter - 1
自增/自减只能应用于变量。
运算符 ++ 和 -- 可以置于变量前,也可以置于变量后。
- 前置型:使用自增后的值(新值);
- 后置型:使用自增前的值(旧值)。
// 递增递减
let num1 = 0;
let num2 = 0;
let result1 = ++num1 + 2; // 3
let result2 = num2++ + 2; // 2
console.log(num2); // 1 后置型:先进行运算,再递增/递减。但计算完成之后,还是会自增的。
速记:前置运算符返回新值,后置运算符返回旧值。
三、关系操作符
- 大于 / 小于:
a > b,a < b。 - 大于等于 / 小于等于:
a >= b,a <= b。 - 检查两个值的相等:
a == b。 - 检查两个值不相等:
a != b。 - 检查两个值全等(值与类型都一致):
a === b。 - 检查两个值不全等(值与类型至少有一个不一致):
a !== b。
所有比较运算符均返回布尔值( true 或 false),比较的结果可以被赋值给任意变量。
1、相等规则
- 左右两侧都是
Number类型的时候- 左右为数值,比较数值大小;
NaN不和任何值相等,包括它本身,返回false;
- 有一侧不是
Number类型的时候- 两边都是
Object类型,比较地址是否一致; null == undefined返回true(undefined衍生自null,但类型不同:typeof null为object,typeof undefined为undefined);- 一侧是
String,一侧是Number,将String转为NaN进行比较,返回NaN; - 一侧是
Number,一侧是true、false、undefined、null,转为Number进行比较。- 注意
null == 0为false,因为null只与undefined相等。
- 注意
- 两边都是
2、字符串比较
字符串是按照 Unicode 编码顺序逐字符地比较大小的,直到比较完成某字符串的所有字符为止。如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。
console.log( 'Z' > 'A' ); // true
console.log( 'A' > 'a' ); // false
console.log( 'Glow' > 'Glee' ); // true
console.log( 'Bee' > 'Be' ); // true
3、对 null 和 undefined 进行比较
- 当使用严格相等
===比较二者时,它们不相等,因为它们属于不同的类型(typeof null为object,typeof undefined为undefined)。
console.log( null === undefined ); // false
- 当使用非严格相等
==比较二者时,JavaScript 存在一个特殊的规则,会判定它们相等null == undefined(undefined衍生自null)。
console.log( null == undefined ); // true
- 当使用数学式或其他比较方法
< > <= >=时:null/undefined会被转化为数字:null被转化为 0,undefined被转化为NaN。
4、相等性检查 与 普通比较符
- 普通比较符进行值的比较时,比较值会被转化为数字。
- 相等性检查
==中不会进行任何的类型转换。
4.1 null
console.log( null > 0 ); // (1) false (0 > 0 => false)
console.log( null == 0 ); // (2) false (null == 0 => false)
console.log( null >= 0 ); // (3) true (0 >= 0 => true)
是的,上面的结果完全打破了你对数学的认识。在最后一行代码显示“null 大于等于 0”的情况下,前两行代码中一定会有一个是正确的,然而事实表明它们的结果都是 false。
为什么会出现这种反常结果,这是因为相等性检查 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的。普通比较符进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false。
另一方面,undefined 和 null 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0 会返回 false。
4.2 undefined
console.log( undefined > 0 ); // false (1)
console.log( undefined < 0 ); // false (2)
console.log( undefined == 0 ); // false (3)
- (1) 和 (2) 都返回 false 是因为
undefined在比较中被转换为了NaN,而NaN是一个特殊的数值型值,它与任何值进行比较都会返回false。 - (3) 返回 false 是因为这是一个相等性检查,而
undefined只与null相等,不会与其他值相等。
4.3 避免问题
- 除了严格相等
===外,其他但凡是有undefined/null参与的比较,我们都需要格外小心。 - 除非你非常清楚自己在做什么,否则永远不要使用
>= > < <=去比较一个可能为null/undefined的变量。对于取值可能是null/undefined的变量,请按需要分别检查它的取值情况。
四、赋值运算符
赋值运算符 =,返回一个值。
let a = 1;
let b = 2;
let c = 3 - (a = b + 1); // 有用,但是可读性差
console.log( a ); // 3
console.log( c ); // 0
1、链式赋值
链式赋值 从右到左 进行计算。
let a, b, c;
a = b = c = 2 + 2;
console.log( a, b, c ); // 4 4 4
2、原地修改
我们经常需要对一个变量做运算,并将新的结果存储在同一个变量中。
所有算术和位运算符都有简短的“修改并赋值”运算符:+= 、 -= 、 /= 、 *= 、 %= 、 **= 等。
这类运算符的优先级与普通赋值运算符的优先级相同,所以它们在大多数其他运算之后执行:
let n = 2;
n *= 3 + 5;
console.log( n ); // 16 (右边部分先被计算,等同于 n *= 8)
五、逻辑运算符
逻辑运算符一共有 3 个:逻辑非(!)、逻辑与(&&)和逻辑或(||)。
-
&&:逻辑与。当两个操作数都是真值时,返回true,否则返回false。- 与运算返回
第一个 假 值,如果没有假值就返回最后一个值作为运算结果
console.log( 1 && 2 && null && 3 ); // null console.log( 1 && 2 && 3 ); // 3,最后一个值 - 与运算返回
-
||:逻辑或。除非两个操作数同时为false,否则结果都返回true。- 或运算返回
第一个 真 值,如果都是假值就返回最后一个值作为运算结果
console.log( null || 1 ); // 1 (1 是第一个真值) console.log( null || 0 || 1 ); // 1 (第一个真值) console.log( undefined || null || 0 ); // 0 (都是假值,返回最后一个值) - 或运算返回
短路求值
逻辑与 和 逻辑或 是 短路操作符,如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。
利用这个特性,可以避免给变量赋值
null或undefined(即 设置默认值)。例如let myObject = preferredObject || backupObject;
- preferredObject 为首选值,backupObject 为备用值。
- 若 preferredObject 不为
null,则它的值就会赋给变量;若 preferredObject 为null,则 backupObject 的值就会赋给变量。
-
!:逻辑非。取反,只作用于一个操作数,结果为布尔值。- 操作数为
Object,返回false(所有对象均为 true);
console.log( !true ); // false console.log( !0 ); // true console.log( !{}) ; // false!!:双重非。相当于Boolean(),用来将某个值转化为布尔类型。第一个!将操作数转为布尔值,第二个!取反。
console.log( !!"non-empty string" ); // true console.log( !!null ); // false - 操作数为
会被转换为 false 的表达式有:空字符串、NaN、0 、null 或 undefined
六、空值合并运算符 ??
在 JavaScript 中,我们将值既不是 null 也不是 undefined 的表达式称为“已定义的(defined)”。
如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数。
空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。
我们可以使用我们已知的运算符重写 result = a ?? b,像这样:
result = (a !== null && a !== undefined) ? a : b;
使用场景:
- 为可能是未定义的变量提供一个默认值。
let user;
// ...
console.log(user ?? "Anonymous"); // (如果 user 有值的话返回 user 的值,否则返回 'Anonymous')
- 从一系列的值中选择出第一个非
null/undefined的值。
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个已定义的值:
console.log(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder
与 || 比较
在上面的代码中,我们可以用 || 替换掉 ??,也可以获得相同的结果:
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个真值:
console.log(firstName || lastName || nickName || "Anonymous"); // Supercoder
|| 运算符自 JavaScript 诞生就存在,因此开发者长期将其用于这种目的。
另一方面,空值合并运算符 ?? 是最近才被添加到 JavaScript 中的,它的出现是因为人们对 || 不太满意。
它们之间重要的区别是:
||返回第一个真值。??返回第一个已定义的值。
换句话说,|| 无法区分 false、0、空字符串 "" 和 null/undefined。它们都一样 —— 假值(falsy values)。如果其中任何一个是 || 的第一个参数,那么我们将得到第二个参数作为结果。
在实际中,我们可能只想在变量的值为 null/undefined 时使用默认值。
let height = 0;
console.log(height || 100); // 100
console.log(height ?? 100); // 0
height || 100首先会检查 height 是否为一个假值,发现它确实是。所以,结果为第二个参数,100。height ?? 100首先会检查 height 是否为 null/undefined,发现它不是。所以,结果为 height 的原始值,0。
如果高度 0 为有效值,则不应将其替换为默认值,所以 ?? 能够得出正确的结果。
优先级
?? 运算符的优先级相当低:在 MDN table 中为 5。因此,?? 在 = 和 ? 之前计算,但在大多数其他运算符(例如,+ 和 *)之后计算。
因此,如果我们需要在还有其他运算符的表达式中使用 ?? 进行取值,需要考虑加括号:
let height = null;
let width = null;
// 重要:使用括号
let area = (height ?? 100) * (width ?? 50);
// 没有括号
// let area = height ?? 100 * width ?? 50;
console.log(area); // 5000
?? 与 && 或 || 一起使用
出于安全原因,JavaScript 禁止将 ?? 运算符与 && 和 || 运算符一起使用,除非使用括号明确指定了优先级。避免人们从 || 切换到 ?? 时的编程错误。
let x = 1 && 2 ?? 3; // Syntax error
let x = (1 && 2) ?? 3; // ok
七、可选链 ?.
可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。
?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(null 或者 undefined) 的情况下不会引起错误,返回 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。
在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符是很有帮助的。
const adventurer = {
cat: {
name: 'Dinah'
}
};
console.log(adventurer.dog?.name); // undefined
console.log(adventurer.cat?.name); // 'Dinah'
// 与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。
console.log(adventurer.someNonExistentMethod?.()); // undefined
八、条件运算符 ?:
语法:
let result = condition ? value1 : value2;
计算条件结果,如果结果为真,则返回 value1,否则返回 value2。
let accessAllowed = (age > 18) ? true : false;
可连续使用多个条件运算符
let age = prompt('age?', 18);
let message = (age < 3) ? 'Hi, baby!' :
(age < 18) ? 'Hello!' :
(age < 100) ? 'Greetings!' :
'What an unusual age!';
console.log( message );
九、逗号运算符
逗号运算符能让我们处理多个语句,使用 , 将它们分开。每个语句都运行了,但是只有最后的语句的结果会被返回。
let a = (1 + 2, 3 + 4);
console.log( a ); // 7(3 + 4 的结果)
逗号运算符的优先级非常低,比 = 还要低,因此上面例子中圆括号非常重要。
如果没有圆括号:a = 1 + 2, 3 + 4 会先执行 +,将数值相加得到 a = 3, 7,然后赋值运算符 = 执行, ‘a = 3’,然后逗号之后的数值 7 不会再执行,它被忽略掉了。相当于 (a = 1 + 2), 3 + 4。
使用场景:把几个行为放在一行上来进行复杂的运算。
// 一行上有三个运算符
for (a = 1, b = 3, c = a * b; a < 10; a++) {
...
}
十、位运算符
位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。
这些运算符不是 JavaScript 特有的。大部分的编程语言都支持这些运算符。
下面是位运算符:
- 按位与 (
&) - 按位或 (
|) - 按位异或 (
^) - 按位非 (
~) - 左移 (
<<) - 右移 (
>>) - 无符号右移 (
>>>)
这些运算符很少被使用,一般是我们需要在最低级别(位)上操作数字时才使用。我们不会很快用到这些运算符,因为在 Web 开发中很少使用它们,但在某些特殊领域中,例如密码学,它们很有用。当你需要了解它们的时候,可以阅读 MDN 上的 位操作符 章节。
十一、运算符优先级
如果一个表达式拥有超过一个运算符,执行的顺序则由 优先级 决定。
在 JavaScript 中有众多运算符。每个运算符都有对应的优先级数字。数字越大,越先执行。如果优先级相同,则按照由左至右的顺序执行。
这是一个摘抄自 MDN 的 优先级表(你没有必要把这全记住,但要记住一元运算符优先级高于二元运算符):
由高到低:
| 优先级 | 运算类型 | 关联性 | 运算符 | 案例 | ||
|---|---|---|---|---|---|---|
| 21 | 圆括号 | n/a(不相关) | ( … ) | 1 + (2 * 3) // 1 + 6 | ||
| 20 | 成员访问 | 从左到右 | … . … 、 … [ … ] | person1.firstname person1['firstname'] | ||
| new (带参数列表) | n/a | new … ( … ) | new Car('Eagle', 1993) | |||
| 函数调用 | 从左到右 | … ( … ) | myFunc(mycar) | |||
| 可选链 | 从左到右 | ?. | adventurer.dog?.name | |||
| 19 | new (无参数列表) | 从右到左 | new … | new Car() | ||
| 18 | 后置递增、递减(运算符在后) | n/a | … ++ 、 … -- | x++ x-- | ||
| 17 | 逻辑非 | 从右到左 | ! … | !true // false | ||
| 按位非 | ~ … | |||||
| 一元加法、减法(正负) | + … 、- … | +1 -2 | ||||
| 前置递增、递减 | ++ … 、-- … | ++x --x | ||||
| typeof | typeof … | typeof 42 // 'number' | ||||
| void | void … | |||||
| delete | delete … | |||||
| await | await … | |||||
| 16 | 幂 | 从右到左 | … ** … | 2 ** 3 // 8 | ||
| 15 | 乘法、除法、取模 | 从左到右 | * 、/ 、% | |||
| 14 | 加法、减法 | 从左到右 | +、- | |||
| 12 | 小于 | … < … | ||||
| 小于等于 | … <= … | |||||
| 大于 | … > … | |||||
| 大于等于 | … >= … | |||||
| in | … in … | |||||
| instanceof | … instanceof … | |||||
| 11 | ==、!=、===、!== | |||||
| 7 | 逻辑与 | … && … | ||||
| 6 | 逻辑或 | … | … | |||
| 5 | 空值合并 | … ?? … | ||||
| 4 | 条件运算符 | 从右到左 | … ? … : … | |||
| 3 | 赋值 | 从右到左 | … = … | |||
| … += … | ||||||
| … -= … | ||||||
| … * * = … | ||||||
| … * = … | ||||||
| … / = … | ||||||
| … %= … | ||||||
| 1 | 展开运算符 | n/a | ... … | |||
| 0 | 逗号 | 从左到右 | … , … |