深入理解红宝书(8)

391 阅读11分钟

第三章 语言基础

这是我参与8月更文挑战的第8天,活动详情查看: 8月更文挑战

今天主要讲解一元运算符和布尔运算符,其中布尔运算符在所有的程序员中用到的比例都是最高的.在我们的一些面试中,布尔运算的判断也是经常会出现,所以新入门的同学一定要认真敲一遍代码,记清楚,别迷糊.

3.5 操作符

ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关 系操作符和相等操作符等。ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、 数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf()和/或 toString()方 法来取得可以计算的值。

3.5.1 一元操作符

只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是 ECMAScript中最简单的操作符。

1. 递增/递减操作符

递增和递减操作符直接照搬自 C 语言,但有两个版本:前缀版和后缀版。顾名思义,前缀版就是位 于要操作的变量前头,后缀版就是位于要操作的变量后头。前缀递增操作符会给数值加 1,把两个加号 (++)放到变量前头即可:

let age = 29; 
++age;

在这个例子中,前缀递增操作符把 age 的值变成了 30(给之前的值 29 加 1)。因此,它实际上等于 如下表达式:

let age = 29; 
age = age + 1;

前缀递减操作符也类似,只不过是从一个数值减 1。使用前缀递减操作符,只要把两个减号(--) 放到变量前头即可:

let age = 29; 
--age; 

执行操作后,变量 age 的值变成了 28(从 29 减 1)。 无论使用前缀递增还是前缀递减操作符,变量的值都会在语句被求值之前改变。(在计算机科学中, 这通常被称为具有副作用。)请看下面的例子:

let age = 29; 
let anotherAge = --age + 2; 
console.log(age); // 28 
console.log(anotherAge); // 30 

在这个例子中,变量 anotherAge 以 age 减 1 后的值再加 2 进行初始化。因为递减操作先发生, 所以 age 的值先变成 28,然后再加 2,结果是 30。 前缀递增和递减在语句中的优先级是相等的,因此会从左到右依次求值。比如:

let num1 = 2; 
let num2 = 20; 
let num3 = --num1 + num2; 
let num4 = num1 + num2; 
console.log(num3); // 21 
console.log(num4); // 21 

这里,num3 等于 21 是因为 num1 先减 1 之后才加 num2。变量 num4 也是 21,那是因为加法使用 的也是递减后的值。 递增和递减的后缀版语法一样(分别是++和--),只不过要放在变量后面。后缀版与前缀版的主要 区别在于,后缀版递增和递减在语句被求值后才发生。在某些情况下,这种差异没什么影响,比如:

let age = 29; 
age++; 

把递增操作符放到变量后面不会改变语句执行的结果,因为递增是唯一的操作。可是,在跟其他操 作混合时,差异就会变明显,比如:

let num1 = 2; 
let num2 = 20; 
let num3 = num1-- + num2; 
let num4 = num1 + num2;
console.log(num3); // 22 
console.log(num4); // 21

这个例子跟前面的那个一样,只是把前缀递减改成了后缀递减,区别很明显。在使用前缀版的例子 中,num3 和 num4 的值都是 21。而在这个例子中,num3 的值是 22,num4 的值是 21。这里的不同之处 在于,计算 num3 时使用的是 num1 的原始值(2),而计算 num4 时使用的是 num1 递减后的值(1)。

这 4 个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可 以。递增和递减操作符遵循如下规则。

  • 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值
  • 对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成 数值。
  • 对于布尔值,如果是 false,则转换为 0 再应用改变。变量类型从布尔值变成数值。
  • 对于布尔值,如果是 true,则转换为 1 再应用改变。变量类型从布尔值变成数值。
  • 对于浮点值,加 1 或减 1。
  • 如果是对象,则调用其(第 5 章会详细介绍的)valueOf()方法取得可以操作的值。对得到的 值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成 数值。

下面的例子演示了这些规则

let s1 = "2"; 
let s2 = "z"; 
let b = false; 
let f = 1.1; 
let o = { 
 valueOf() { 
 return -1; 
 } 
}; 
s1++; // 值变成数值 3 
s2++; // 值变成 NaN 
b++; // 值变成数值 1 
f--; // 值变成 0.10000000000000009(因为浮点数不精确)
o--; // 值变成-2

2. 一元加和减

一元加和减操作符对大多数开发者来说并不陌生,它们在 ECMAScript 中跟在高中数学中的用途一 样。一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响:

let num = 25; 
num = +num; 
console.log(num); // 25

如果将一元加应用到非数值,则会执行与使用 Number()转型函数一样的类型转换:布尔值 false 和 true 转换为 0 和 1,字符串根据特殊规则进行解析,对象会调用它们的 valueOf()和/或 toString() 方法以得到可以转换的值。

下面的例子演示了一元加在应用到不同数据类型时的行为:

let s1 = "01"; 
let s2 = "1.1";
let s3 = "z"; 
let b = false; 
let f = 1.1; 
let o = { 
 valueOf() { 
 return -1; 
 } 
}; 
s1 = +s1; // 值变成数值 1 
s2 = +s2; // 值变成数值 1.1 
s3 = +s3; // 值变成 NaN 
b = +b; // 值变成数值 0 
f = +f; // 不变,还是 1.1 
o = +o; // 值变成数值-1 

一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值,如把 1 转换为-1。示例 如下:

let num = 25; 
num = -num; 
console.log(num); // -25 

对数值使用一元减会将其变成相应的负值(如上面的例子所示)。在应用到非数值时,一元减会遵 循与一元加同样的规则,先对它们进行转换,然后再取负值:

let s1 = "01"; 
let s2 = "1.1"; 
let s3 = "z"; 
let b = false; 
let f = 1.1; 
let o = { 
 valueOf() { 
 return -1; 
 } 
}; 
s1 = -s1; // 值变成数值-1 
s2 = -s2; // 值变成数值-1.1 
s3 = -s3; // 值变成 NaN 
b = -b; // 值变成数值 0 
f = -f; // 变成-1.1 
o = -o; // 值变成数值 1 

一元加和减操作符主要用于基本的算术,但也可以像上面的例子那样,用于数据类型转换。

3.5.2 布尔操作符

对于编程语言来说,布尔操作符跟相等操作符几乎同样重要。如果没有能力测试两个值的关系,那 么像 if-else 和循环这样的语句也没什么用了。布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或。

1. 逻辑非

逻辑非操作符由一个叹号(!)表示,可应用给 ECMAScript 中的任何值。这个操作符始终返回布 尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。换 句话说,逻辑非操作符会遵循如下规则。

  • 如果操作数是对象,则返回 false。
  • 如果操作数是空字符串,则返回 true。
  • 如果操作数是非空字符串,则返回 false。
  • 如果操作数是数值 0,则返回 true。
  • 如果操作数是非 0 数值(包括 Infinity),则返回 false。
  • 如果操作数是 null,则返回 true。
  • 如果操作数是 NaN,则返回 true。
  • 如果操作数是 undefined,则返回 true。

以下示例验证了上述行为:

console.log(!false); // true 
console.log(!"blue"); // false 
console.log(!0); // true 
console.log(!NaN); // true 
console.log(!""); // true 
console.log(!12345); // false 

逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于调用了转型函 数 Boolean()。无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反, 从而给出变量真正对应的布尔值。结果与对同一个值使用 Boolean()函数是一样的:

console.log(!!"blue"); // true 
console.log(!!0); // false 
console.log(!!NaN); // false 
console.log(!!""); // false 
console.log(!!12345); // true

2. 逻辑与

逻辑与操作符由两个和号(&&)表示,应用到两个值,如下所示

let result = true && false;

逻辑与操作符遵循如下真值表:

第一个操作数第二个操作数结 果
truetruefalse
truefalsefalse
falsetruefalse
falsefalsefalse

逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不 一定会返回布尔值,而是遵循如下规则。

  • 如果第一个操作数是对象,则返回第二个操作数。
  • 如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是 null,则返回 null。
  • 如果有一个操作数是 NaN,则返回 NaN。
  • 如果有一个操作数是 undefined,则返回 undefined。

逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个 操作数求值。对逻辑与操作符来说,如果第一个操作数是 false,那么无论第二个操作数是什么值,结 果也不可能等于 true。看下面的例子:

let found = true; 
let result = (found && someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行

上面的代码之所以会出错,是因为 someUndeclaredVariable 没有事先声明,所以当逻辑与操作符 对它求值时就会报错。变量 found 的值是 true,逻辑与操作符会继续求值变量 someUndeclaredVariable。 但是由于 someUndeclaredVariable 没有定义,不能对它应用逻辑与操作符,因此就报错了。假如变 量 found 的值是 false,那么就不会报错了:

let found = false; 
let result = (found && someUndeclaredVariable); // 不会出错
console.log(result); // 会执行

这里,console.log 会成功执行。即使变量 someUndeclaredVariable 没有定义,由于第一个 操作数是 false,逻辑与操作符也不会对它求值,因为此时对&&右边的操作数求值是没有意义的。在使 用逻辑与操作符时,一定别忘了它的这个短路的特性。

3. 逻辑或

逻辑或操作符由两个管道符(||)表示,比如:

let result = true || false;

逻辑或操作符遵循如下真值表:

第一个操作数第二个操作数结 果
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如 下规则。

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为 false,则返回第二个操作数。
  • 如果两个操作数都是对象,则返回第一个操作数。
  • 如果两个操作数都是 null,则返回 null。
  • 如果两个操作数都是 NaN,则返回 NaN。
  • 如果两个操作数都是 undefined,则返回 undefined。

同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为 true,第二个操作数就不会再被求值了。看下面的例子:

let found = true; 
let result = (found || someUndeclaredVariable); // 不会出错
console.log(result); // 会执行

跟前面的例子一样,变量 someUndeclaredVariable 也没有定义。但是,因为变量 found 的值 为 true,所以逻辑或操作符不会对变量 someUndeclaredVariable 求值,而直接返回 true。假如把 found 的值改为 false,那就会报错了:

let found = false; 
let result = (found || someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行

利用这个行为,可以避免给变量赋值 null 或 undefined。比如:

let myObject = preferredObject || backupObject; 

在这个例子中,变量 myObject 会被赋予两个值中的一个。其中,preferredObject 变量包含首 选的值,backupObject 变量包含备用的值。如果 preferredObject 不是 null,则它的值就会赋给 myObject;如果 preferredObject 是 null,则 backupObject 的值就会赋给 myObject。这种模 式在 ECMAScript 代码中经常用于变量赋值,后面的代码示例中也会经常用到。