TypeScript运算符实战:数学运算、比较运算、逻辑运算与三元运算符
对 TypeScript 的算术运算符、赋值运算符、比较运算符、逻辑运算符、递增运算符以及三元运算符进行实用讲解,包括编译器不允许使用的相等性检查。
一旦了解了 TypeScript 如何分配和强制类型,下一步就是对这些值进行操作:添加它们、比较它们、组合条件并在不同选项间选择。运算符就是执行这些操作的符号,其中大多数在行为上与 JavaScript 完全一致。不过也有少数运算符会隐藏一些常导致错误的细节,比如宽松相等与严格相等的区别,以及 x++ 和 ++x 的差异。
本指南将通过一些无需实际运行即可理解的简单示例,介绍算术运算符、赋值运算符、关系运算符、逻辑运算符、递增/递减运算符以及三元运算符。同时你也会了解到 TypeScript 编译器在哪些地方加入了 JavaScript 所没有的安全机制。
先看快捷方式:在一条语句中声明多个变量
在探讨运算符之前,先介绍一个后续示例会用到的语法便利特性。通常声明两个数字的方式是为每个变量写一条语句:
let a: number = 10;
let b: number = 20;
如果用逗号分隔,单个 let 语句也可以一次性定义多个变量:
let a: number = 10, b: number = 20;
这两种形式编译后的结果相同,只是第二种更简洁。列表中的每个变量都有独立的注解,因此它们可以具有不同的类型:
let a: number = 10, name: string = "John", isActive: boolean = true;
请谨慎使用这种形式。对于像本指南中反复出现的 a 和 b 这样紧密相关的值对,这种写法更易理解;但若一行中同时包含计数器、名称和标志符,其可读性就会低于三行独立的声明。
算术运算符
这些就是我们在学校数学课上见过的常见符号。下面的示例都是从两个数字开始展开的:
// Arithmetic operators
let a: number = 10, b: number = 20;
每行都会应用一个运算符,并在注释中显示结果:
console.log(a + b); // 30 → addition
console.log(a - b); // -10 → subtraction
console.log(a * b); // 200 → multiplication
console.log(a / b); // 0.5 → division
console.log(a % b); // 10 → modulus (remainder after division)
console.log(a ** b); // 100000000000000000000 → exponentiation (10 to the power 20)
各运算符的功能如下:
+用于将两个操作数相加。-用左操作数减去右操作数。*用于将两个操作数相乘。/进行除法运算且不四舍五入。10 / 20的结果确实是0.5;由于 JavaScript 中的所有数字都是浮点数,因此不存在像其他某些语言那样的单独整数除法功能。%用于返回余数。10 % 20的结果是10:20 无法整除 10,因此全部 10 都作为余数保留下来。**用于将左操作数提升到右操作数的次方。10 ** 20表示 10 自身相乘 20 次,这也解释了为何结果会如此之长。
使用 ** 运算符处理极大数值时需注意:超过 Number.MAX_SAFE_INTEGER(约为九千万亿)的数值会失去整数精度。10 ** 20 能够准确显示,但对该规模数值进行算术运算并不可靠;如需精确的大整数,请使用 bigint。
赋值与复合赋值
普通的 = 运算符用于将值存储在变量中。JavaScript 和 TypeScript 还提供了复合赋值运算符,它可以在单个表达式中完成算术运算并存储结果。首先准备两个值:
let a: number = 100, b: number = 50;
然后分别用两种方式两次更新 a 的值,一次使用常规方法,一次使用简写形式:
console.log(a); // 100
a = a + b;
console.log(a); // 150
console.log(a += b); // 200
逐步说明:a的初始值为100。语句a = a + b会读取当前的a值,再加上b的数值(50),然后将结果150写回a中。这与前一节中的+ 运算符用法并无不同。
最后一行使用了a += b,其含义与a = a + b完全相同,只是写法更为简洁。由于此时a的值为150,因此执行后其值变为200。赋值语句本身也是一种表达式,其返回值为新存储的数值,这就是为什么将a += b直接传递给console.log时会输出200。虽然这样可行,但将状态变化隐藏在函数参数中会降低代码的可读性,因此在实际编码中建议将更新操作单独写在一行上。
所有的算术运算符都遵循相同的模式:-=、*=、/=、%=和**=分别执行减法、乘法、除法、取余运算或幂运算,之后再将结果赋值。
关系运算符
比较运算符用于对比两个值,其结果始终为布尔值,即true或false。以下是这些运算符的操作数:
let a: number = 10;
let b: number = 20;
以及相应的比较操作:
console.log(a > b); // false
console.log(a < b); // true
console.log(a === b); // false
console.log(a >= b); // false
console.log(a <= b); // true
console.log(a == b); // false
console.log(a != b); // true
比较运算符的含义很直观:>(大于)、<(小于)、>=(大于或等于)以及 <=(小于或等于)。相等运算符 ==、=== 和 != 用于判断两个值是否相同,但 == 和 === 遵循不同的规则,这一差异值得单独说明。!= 的严格对应运算符是 !==,通常情况下应优先使用它。
宽松相等与严格相等
这两种相等运算符在处理类型时存在差异:
==属于宽松相等。如果操作数的类型不同,它会在比较值之前默默地将其中一个转换。
===表示严格相等性。它要求类型和值都完全相同,不会进行任何转换,一旦类型不同就会返回false。为便于说明,我们取一个数字和一个外观相似的字符串。两者都被刻意标记为any类型,原因稍后会解释。
let num1: any = 10;
let num2: any = "10";
使用不同的运算符进行比较会得到不同的结果:
console.log(num1 == num2); // true → same value, type is ignored
console.log(num1 === num2); // false → same value, but number ≠ string
num1存储数字10,而num2存储字符串"10"。宽松相等性会将字符串转换为数字,比较两侧的数值是否为10,若相同则返回true。严格相等性则完全跳过转换过程;数字永远不可能等于字符串,因此结果为false。由于宽松相等性的转换规则会产生意外结果(例如0 == ""的值为true),大多数风格指南和代码检查工具都建议在所有情况下使用===和!==。
为何该示例需要使用any
any 注解并非随意添加的。如果你用实际类型声明变量,如 let num1: number = 10; 和 let num2: string = "10";,然后再编写 num1 === num2,代码将无法编译。编译器会报出类似如下的错误:
This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.
TypeScript 知道类型为 number 的值与类型为 string 的值永远不可能相同,因此会将该比较视为错误。这一检查同样适用于 == 和 ===。若将两个变量都定义为 any,则不会进行此类重叠分析,这也是该示例能够编译的唯一原因。这是一个简短但实用的例子,展示了类型系统如何在程序运行前捕获无意义的比较,而普通 JavaScript 是不会发出此类警告的。它同时也说明了为何使用 any 会带来弊端:因为它会悄悄关闭这类保护机制。如需替代方案,请参阅 六种针对常见场景的 any 类型安全替代方案。
逻辑运算符
逻辑运算符用于组合条件。共有三种:&&(与)、||(或)以及 !(非)。首先从两个布尔值开始:
let b1 = true;
let b2 = false;
然后对它们进行组合和取反操作:
console.log(b1 && b2); // false
console.log(b1 || b2); // true
console.log(!b2); // true
console.log(!b1); // false
每对输入对应的与运算和或运算的完整行为如下:
true和true:&&的结果为true,||的结果也为true。true和false:&&的结果为false,||的结果为true。false和true:&&的结果为false,||的结果为true。false和false:&&的结果为false,||的结果也为false。
对于 NOT 运算:
!true的结果是false。!false的结果是true。
用文字解释:
&&(与) 是严格的运算符。只有当两侧都为true时才会返回true;只要有一侧为false,整个表达式的结果就是false。||(或) 是宽松的运算符。只要有一侧为true即可,只有当两侧都为false时才会返回false。!(非) 接受一个操作数并将其取反:true变为false,反之亦然。
一个值得了解的细节:! 总是返回布尔值,而 && 和 || 实际上会返回其操作数中的一个。当输入为布尔值时,结果仍为布尔值,如前所述;对于其他类型的值,"default" || "someValue" 的结果为 "default",user && user.name 的结果则为 user 或 user.name。这两种运算符还具有短路特性:如果左侧已经能确定结果,右侧就不会被计算。正因如此,它们常被用于默认值处理和条件判断,TypeScript 也会相应地对结果进行类型定义。
将比较运算与逻辑运算结合使用
关系运算符和逻辑运算符通常会一起出现:比较运算产生布尔值,而逻辑运算则用于组合这些布尔值。
console.log(20 > 10 && 20 < 10); // false
console.log(20 > 10 || 20 < 10); // true
在第一行中,20 > 10 的结果为 true,而 20 < 10 的结果为 false。由于“与”运算需要两个操作数都为 true,因此最终结果为 false。
在第二行中,两个比较表达式相同,但“或”运算只需其中一个操作数为 true 即可,因此结果为 true。因为比较运算符的结合性比 && 和 || 更强,所以这里不需要加括号,不过加上括号可以让更复杂的条件更易于理解。
递增与递减
++ 用于给数字加1,-- 用于减1。这两种操作符都有前缀形式和后置形式,二者之间的区别常常让人困惑。我们先从单个变量开始讲解:
let x: number = 10;
当该运算符作为独立语句使用时,两种形式的效果是相同的:
x++; // same as x = x + 1
console.log(x); // 11
++x; // same as x = x + 1
console.log(x); // 12
x++和++x如果单独成行书写,都会将1加到x上。这两种形式只有在表达式的值被用于同一条语句中时才会表现不同,例如当将其赋值给另一个变量时。此时前缀递增和后置递增的区别才显现出来。
后置递增:先使用值,再加1
“后置”意味着在之后执行:表达式首先计算出变量的当前值,随后再进行递增操作。
let x: number = 10;
let res: number = x++;
打印这两个变量可以显示其效果:
console.log(res); // 10
console.log(x); // 11
事件发生的顺序为:
- 读取
x的当前值,即10。 - 该值成为表达式的结果并存储在
res中。 - 随后
x的值增加为11。
因此 res 保留旧值 10,而 x 变为 11。递增操作确实发生了,只是发生在值被复制之后。
前缀递增:先加一,再使用该值
“Pre”表示在之前:首先对变量进行递增,表达式的计算结果即为更新后的值。
let x: number = 10;
let res: number = ++x;
输出结果可以证实这一点:
console.log(res); // 11
console.log(x); // 11
这里的顺序则相反:
x从10变为11。- 新值
11被存储在res中。
两个变量的最终值都是 11。简而言之:x++ 会返回递增前的值,而 ++x 会返回递增后的值。
递减也遵循相同的规则
-- 的作用与 ++ 相同,只是执行减法而非加法。前缀形式的写法如下:
let x: number = 10;
let res: number = --x;
其输出结果为:
console.log(res); // 9
console.log(x); // 9
使用前缀递减运算时,x 首先变为 9,这个新值会被赋给 res,因此两者都会输出 9。而后缀形式 x-- 的效果则相反:res 会保留原来的 10,之后 x 才会变为 9。许多团队为了避免混淆,完全避免在复杂表达式中使用 ++ 和 --;而单独一行写的 x += 1 则绝不会产生歧义。
三元条件运算符
三元运算符可以将简单的 if/else 判断表示为单个表达式。它使用 ? 和 :,也是唯一一个需要三个操作数的 JavaScript 运算符,其名称便由此而来。我们先从两个数字开始:
let a: number = 100;
let b: number = 200;
然后选出较大的那个数:
let res: number = (a > b) ? a : b;
console.log(res); // 200
该表达式由三部分组成:
?之前的(a > b)是判断条件。?之后的a是当条件为true时的结果。:之后的b是当条件为false时的结果。
大声读出来,上面写着:如果 a 大于 b,则使用 a,否则使用 b。由于 100 不大于 200,因此条件为 false,res 的值变为 200。
替代 if/else 语句块
其主要优点在于简洁性。用普通语句实现相同逻辑需要多行代码,而且还要在赋值之前先声明 res:
// Without ternary
let res: number;
if (a > b) {
res = a;
} else {
res = b;
}
三元运算符版本则只需一行即可完成相同功能。这两种写法是互为替代的,并非可以放在同一作用域中的代码,因为用 let 两次声明 res 会引发错误:
// With ternary — same result, one line
let res: number = (a > b) ? a : b;
由于三元运算符本身是一个表达式,它允许你在一步之内同时声明变量并对其进行初始化,而且 TypeScript 会自动推断或检查两个分支所产生的结果的类型。
将值映射为标签
当分支类型的条件不同时,三元运算符依然能正常使用。此示例将年龄转换为描述性字符串:
let personage: number = 30;
let res: string = (personage > 18) ? "adult" : "minor";
console.log(res); // "adult"
30 > 18 的结果为 true,因此 res 的值为 "adult"。若年龄为 15,则条件结果为 false,res 的值为 "minor"。
对于这类简单、单一的判断,建议使用三元运算符。但一旦开始在其中一个三元运算符内部嵌套另一个以处理多种情况,代码可读性会迅速下降,此时使用 if...else if...else 语句链(或查找对象)通常会更清晰。
关键要点
- TypeScript 的运算符与 JavaScript 的运算符相同;编译后的代码在运行时的行为也完全一致。
/ 永不截断,% 返回余数,而 ** 计算出的极大结果会失去精度,除非使用 bigint。+= 这样的复合赋值运算符是“计算并存储回原变量”的简写形式。=== 和 !==;宽松相等运算符会以出人意料的方式转换类型。any 会悄悄移除这种保护机制。&& 和 || 具有短路特性,会直接返回其中一个操作数,这也是它们能同时用作保护条件和默认值的原因。x++ 返回的是旧值,前置运算符 ++x 返回的是新值;只有在使用结果时才能看出两者的区别。if...else 结构。