JavaScript 基础(四):操作符

298 阅读9分钟

一、运算元

运算元 —— 运算符应用的对象。比如说乘法运算 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 ** ba 乘以自身 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 > ba < b
  • 大于等于 / 小于等于:a >= ba <= b
  • 检查两个值的相等:a == b
  • 检查两个值不相等: a != b
  • 检查两个值全等(值与类型都一致)a === b
  • 检查两个值不全等(值与类型至少有一个不一致): a !== b

所有比较运算符均返回布尔值( truefalse,比较的结果可以被赋值给任意变量。

1、相等规则

  • 左右两侧都是 Number 类型的时候
    • 左右为数值,比较数值大小;
    • NaN 不和任何值相等,包括它本身,返回 false
  • 有一侧不是 Number 类型的时候
    • 两边都是 Object 类型,比较地址是否一致
    • null == undefined 返回 trueundefined 衍生自 null ,但类型不同:typeof nullobjecttypeof undefinedundefined);
    • 一侧是 String ,一侧是 Number ,将 String 转为 NaN 进行比较,返回 NaN ;
    • 一侧是 Number ,一侧是 true、false、undefined、null,转为 Number 进行比较
      • 注意 null == 0false,因为 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、对 nullundefined 进行比较

  • 当使用严格相等 === 比较二者时,它们不相等,因为它们属于不同的类型(typeof nullobjecttypeof undefinedundefined)。
console.log( null === undefined ); // false
  • 当使用非严格相等 == 比较二者时,JavaScript 存在一个特殊的规则,会判定它们相等 null == undefinedundefined 衍生自 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

另一方面,undefinednull 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(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 (都是假值,返回最后一个值)
    

短路求值

逻辑与 和 逻辑或 是 短路操作符如果第一个操作数决定了结果,那么永远不会对第二个操作数求值

利用这个特性,可以避免给变量赋值 nullundefined(即 设置默认值)。例如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 、nullundefined

六、空值合并运算符 ??

在 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/anew … ( … )new Car('Eagle', 1993)
函数调用从左到右… ( … )myFunc(mycar)
可选链从左到右?.adventurer.dog?.name
19new (无参数列表)从右到左new …new Car()
18后置递增、递减(运算符在后)n/a… ++ 、 … --x++ x--
17逻辑非从右到左! …!true // false
按位非~ …
一元加法、减法(正负)+ … 、- …+1 -2
前置递增、递减++ … 、-- …++x --x
typeoftypeof …typeof 42 // 'number'
voidvoid …
deletedelete …
awaitawait …
16从右到左… ** …2 ** 3 // 8
15乘法、除法、取模从左到右* 、/ 、%
14加法、减法从左到右+、-
12小于… < …
小于等于… <= …
大于… > …
大于等于… >= …
in… in …
instanceof… instanceof …
11==、!=、===、!==
7逻辑与… && …
6逻辑或
5空值合并… ?? …
4条件运算符从右到左… ? … : …
3赋值从右到左… = …
… += …
… -= …
… * * = …
… * = …
… / = …
… %= …
1展开运算符n/a... …
0逗号从左到右… , …