Logo
Section 1 / 9

比较变量

本课程讲解了 TypeScript 如何比较变量。

导入

比较变量

本课程讲解了 TypeScript 如何比较变量。

按值比较还是按引用比较?

TypeScript 继承了 JavaScript 的宽松比较规则。正如我们之前所见,TypeScript 通过增加严格性来减少潜在的 JavaScript 陷阱。然而,并没有强制开发者使用“三重等号”(===)进行比较,而这一点对于避免使用“双等号”(==)时可能发生的类型转换至关重要。TypeScript 通过消除一些边缘情况来帮助处理“双等号”的问题。

例如,在 JavaScript 中,将一个 number 类型的值与字符串形式的数字进行比较是可行的,但在 TypeScript 中则无法编译。在 第1行,我们声明了一个字符串变量,其值为 "1";在 第2行,我们定义了一个数值为 1 的 number 类型变量。第3行的代码在 TypeScript 中无法通过编译,因为字符串和数字是不同类型,TypeScript 不允许直接用 == 比较它们。

📜 注意:以下代码会报错 ❌

1let value1String: string = "1"; 2let value1Number: number = 1; 3if (value1String == value1Number) { // 问题所在 4 console.log("TypeScript 不会编译"); 5}

基本类型按值比较。另一方面,对象字面量、类实例和数组都按引用比较。

在下面的代码中,第3行比较了两个结构和值完全相同的对象。然而它们并不相等,原因在于这两个对象是在不同位置初始化的(第1行和第2行),因此它们是不同的引用。

1const value1Object = { m1: "test" }; // 非基本类型:对象 2const value2Object = { m1: "test" }; // 非基本类型:对象 3if (value1Object === value2Object) { // 引用比较 4 console.log("不应该打印这一行"); 5}

比较两个对象

TypeScript 使用结构化类型系统,而非名义类型系统。这意味着类型是根据其结构进行比较,而不是根据名称。C# 或 Java 等主流语言采用名义类型,类似 TypeScript 的 Flow 语言也是如此。名义比较仅在设计时发生,因为 JavaScript 本身没有类型信息。TypeScript 在运行时会移除所有类型信息,因此无法实现基于类型名称的比较。相比之下,TypeScript 所采用的结构化比较机制在设计时和运行时均可使用。

我们已经了解如何使用 typeof 和 instanceof 进行类型检查。但值得注意的是,TypeScript 在比较方面依然保持了灵活性。它不允许你仅仅提供精确的类型就能通过比较检查。

关键在于尊重类型的结构。这种对结构的关注使得任何具有最小必要字段交集的结构都可以被接受。

例如,如果某个函数要求参数是一个名为 A 的接口,且该接口包含一个名为 B 的成员,那么你可以传递任何具有相同名称 B 且类型一致的接口、类型或对象字面量。

1interface ParameterType { 2 m1: number; 3} 4interface NotRelatedType { 5 m1: number; 6 m2: string; 7 m3: boolean; 8} 9interface NotRelatedTypeNoM1Number { 10 m1: string; // 成员名与 "ParameterType" 相同,但类型为 string 11} 12 13const i1: ParameterType = { m1: 1 }; 14const i2: NotRelatedType = { m1: 1, m2: "1", m3: true }; 15const i3: NotRelatedTypeNoM1Number = { m1: "1" }; 16 17function IWantParameterType(p: ParameterType): void {} 18IWantParameterType(i1); 19IWantParameterType(i2); 20// IWantParameterType(i3); // 编译失败

上述代码中,第一个接口 ParameterType 仅包含一个成员 m1: number。第二个接口也包含相同的 m1,但额外定义了两个成员(第6-7行)。

第13至15行分别初始化了每个接口的变量。第17行定义了一个接收 ParameterType 类型参数的函数。可以看到,第18行传入了精确匹配的类型,成功通过。同样地,NotRelatedType 也被接受,尽管其结构不完全相同。但由于它包含了 ParameterType 所需的所有字段,且 TypeScript 是基于结构的类型系统,因此满足函数的要求。然而,第20行被注释掉,因为它会导致错误。问题在于 NotRelatedTypeNoM1Number 的 m1 字段类型为 string,与预期的 number 不兼容。

与 null 和 undefined 的比较

使用“三重等号”时,null 和 undefined 是不同的;但使用“双等号”时,它们是相等的。在 TypeScript 中,undefined 用于表示可选值,而 null 表示“此处无值”。目前有一种趋势是尽量只使用 undefined,完全避免 null。但这并非绝对可靠。

一种常见的比较 null 或 undefined 的模式是直接检查变量是否存在,而不使用任何操作符。然而,当变量类型为 number 或 Boolean 时,这种写法存在问题。

例如,Boolean 可能是 undefined 或 false,在这两种情况下都会进入同一个条件分支。虽然混淆 null 或 undefined 与实际值的情况并不常见,但代码往往会演进。一个函数的返回值可能最初是对象,后来变为 Boolean,这仍然可以编译通过,但会改变条件判断的行为。

1function fct(): Boolean { 2 return new Boolean(false); // false 3} 4if (fct()) { 5 console.log("值为 true"); // 仍会输出,因为对象存在 6}

最终,为了编写健壮的代码,多打几个字符以确保安全是值得的。理想的做法是先用“三重等号”检查 undefined,再检查 null(如果两者都可能)。

下面是一个展示 false 和 undefined 混合使用的例子。第1行将变量初始化为 undefined,但也可以是 true 或 false,因为它的类型是 boolean | undefined。省略等号的简写形式会进入第一个条件,但这可能不是我们期望的结果——我们本希望仅在明确为 false 时才进入。

1let value2Boolean1: boolean | undefined = undefined; 2let value2Boolean2: boolean | undefined = true; 3let value2Boolean3: boolean | undefined = false; 4if (!value2Boolean1) { 5 console.log("这是 false 1"); 6} 7if (!value2Boolean2) { 8 console.log("这是 false 2"); 9} 10if (!value2Boolean3) { 11 console.log("这是 false 3"); 12}

这种比较方式的隐患在于代码演变过程中。比如最初只处理 boolean 类型,此时条件是否为 false 很清楚,但后续可能扩展为允许 undefined、null,甚至 0,从而导致逻辑错误。

使用“双等号”进行比较

第二种更少输入的方式是使用“双等号”来同时检查 null 和 undefined。在这种情况下,验证会将对象值与 null 或 undefined 进行比较。虽然仍使用“双等号”,但比直接与空值比较并让对象或原始值被转换为布尔值要好得多。

一个重要配置是将 strictNullChecks 设置为 true。这个选项在 tsconfig.json 中启用后,会在 null 和 undefined 之间建立明确界限。

注意:以下代码能正常工作,是因为本课程的 tsconfig.json 已启用相关设置

1const x: boolean | undefined = undefined; 2if (x == undefined) { 3 console.log("x 为 undefined"); 4} 5if (x == false) { // 如果 strictNullChecks 为 false,此条件也会进入 6 console.log("x 为 false"); 7}

如今,使用“三重等号”直接比较 undefined 是完全安全的。很久以前,在 ECMAScript 5 之前,undefined 可以被重新定义,因此直接比较 undefined 不安全。这也是为什么当时推荐使用 typeof 并与字符串字面量 "undefined" 比较。此外,也可以使用 void 0 与“三重等号”比较。原因是当被比较的变量值为 undefined 时,它等于 void 0。但在 TypeScript 中,最清晰、最推荐的方式是直接使用“三重等号”与变量 undefined 比较。

1const x: string | undefined = undefined; 2if (x === undefined) { 3 console.log("x 为 undefined"); 4} 5if (typeof x === "undefined") { 6 console.log("x 为 undefined(通过 typeof)"); 7} 8if (x === void 0) { 9 console.log("x 为 void 0"); 10}

TypeScript 继承了 JavaScript 的灵活性,具体如何使用比较方式取决于开发者的选择。作为良好实践,始终使用“三重等号”与预期的精确类型进行比较,是最安全的模式。

别名

使用 typeof 进行类型检查