首页 / 文章 / TypeScript运算符实战:数学运算、比较运算、逻辑运算与三元运算符

TypeScript运算符实战:数学运算、比较运算、逻辑运算与三元运算符

对 TypeScript 的算术运算符、赋值运算符、比较运算符、逻辑运算符、递增运算符以及三元运算符进行实用讲解,包括编译器不允许使用的相等性检查。

2635 词

一旦了解了 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
    

    事件发生的顺序为:

    1. 读取x的当前值,即10。
    2. 该值成为表达式的结果并存储在res中。
    3. 随后x的值增加为11。

    因此 res 保留旧值 10,而 x 变为 11。递增操作确实发生了,只是发生在值被复制之后。

    前缀递增:先加一,再使用该值

    “Pre”表示在之前:首先对变量进行递增,表达式的计算结果即为更新后的值。

    let x: number = 10;
    let res: number = ++x;
    

    输出结果可以证实这一点:

    console.log(res); // 11
    console.log(x);   // 11
    

    这里的顺序则相反:

    1. x 从 10 变为 11。
    2. 新值 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
    

    该表达式由三部分组成:

    1. ? 之前的 (a > b) 是判断条件。
    2. ? 之后的 a 是当条件为 true 时的结果。
    3. : 之后的 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 结构。