Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

面向 Java/C# 程序员的 TypeScript

TypeScript 是习惯于 C# 和 Java 等静态类型语言程序员的流行选择。

TypeScript 的类型系统提供了许多相同的好处,例如更好的代码补全、更早的错误检测以及程序各部分之间更清晰的通信。 虽然 TypeScript 为这些开发者提供了许多熟悉的功能,但值得退一步看看 JavaScript(以及因此 TypeScript)与传统面向对象编程语言的不同之处。 理解这些差异将帮助你编写更好的 JavaScript 代码,并避免那些直接从 C#/Java 转到 TypeScript 的程序员可能遇到的常见陷阱。

共同学习 JavaScript

如果你已经熟悉 JavaScript,但主要是一名 Java 或 C# 程序员,本介绍页可以帮助解释一些你可能容易产生的常见误解和陷阱。 TypeScript 建模类型的一些方式与 Java 或 C# 相当不同,在学习 TypeScript 时牢记这些非常重要。

如果你是一名 Java 或 C# 程序员,但对 JavaScript 整体来说还是新手,我们建议先学习一点不带类型的 JavaScript,以了解 JavaScript 的运行时行为。 因为 TypeScript 不会改变代码运行的方式,你仍然需要学习 JavaScript 的工作原理才能编写出真正做事情的代码!

重要的是要记住 TypeScript 使用与 JavaScript 相同的运行时,因此关于如何实现特定运行时行为(将字符串转换为数字、显示警报、将文件写入磁盘等)的任何资源都将同样适用于 TypeScript 程序。 不要将自己限制在特定于 TypeScript 的资源中!

重新思考类

C# 和 Java 可以说是强制性面向对象编程语言。 在这些语言中,是代码组织的基本单元,也是运行时所有数据行为的基本容器。 将所有功能和数据强制放在类中对于某些问题可能是一个好的领域模型,但并非每个领域需要以这种方式表示。

自由函数和数据

在 JavaScript 中,函数可以存在于任何地方,数据可以自由传递,而无需位于预定义的 classstruct 内部。 这种灵活性非常强大。 “自由”函数(那些不与类关联的函数)在数据上工作,没有隐含的面向对象编程层次结构,这往往是 JavaScript 中编写程序的首选模型。

静态类

此外,C# 和 Java 中的某些构造,如单例和静态类,在 TypeScript 中是不必要的。

TypeScript 中的面向对象编程

话虽如此,如果你愿意,仍然可以使用类! 某些问题非常适合通过传统的面向对象编程层次结构来解决,而 TypeScript 对 JavaScript 类的支持将使这些模型更加强大。 TypeScript 支持许多常见模式,例如实现接口、继承和静态方法。

我们将在本指南的后面部分介绍类。

重新思考类型

TypeScript 对类型的理解实际上与 C# 或 Java 有很大不同。 让我们探讨一些差异。

名义具象化类型系统

在 C# 或 Java 中,任何给定的值或对象都有一个确切的类型——要么是 null,要么是原始类型,要么是已知的类类型。 我们可以调用像 value.GetType()value.getClass() 这样的方法在运行时查询确切的类型。 此类型的定义将位于某个具有某个名称的类中,除非存在显式的继承关系或共同实现的接口,否则我们不能用具有相似形状的两个类相互替代。

这些方面描述了一个具象化的、名义的类型系统。 我们在代码中编写的类型在运行时存在,并且类型通过其声明(而非结构)相关联。

作为集合的类型

在 C# 或 Java 中,将运行时类型与其编译时声明视为一一对应是有意义的。

在 TypeScript 中,最好将类型视为共享某些共同点的值的集合。 因为类型只是集合,所以一个特定的值可以同时属于多个集合。

一旦你开始将类型视为集合,某些操作就变得非常自然。 例如,在 C# 中,传递一个要么string 要么int 的值是很尴尬的,因为没有单一类型可以表示这种值。

在 TypeScript 中,一旦你意识到每个类型只是一个集合,这就会变得非常自然。 如何描述一个要么属于 string 集合要么属于 number 集合的值? 它只是属于这些集合的并集string | number

TypeScript 提供了许多以集合论方式处理类型的机制,如果你将类型视为集合,你会发现它们更直观。

擦除的结构类型

在 TypeScript 中,对象不是单一的精确类型。 例如,如果我们构造一个满足接口的对象,即使两者之间没有声明性关系,我们也可以在该接口期望的地方使用该对象。

ts
interface Pointlike {
  
x
: number;
y
: number;
} interface Named {
name
: string;
} function
logPoint
(
point
: Pointlike) {
console
.
log
("x = " +
point
.
x
+ ", y = " +
point
.
y
);
} function
logName
(
x
: Named) {
console
.
log
("Hello, " +
x
.
name
);
} const
obj
= {
x
: 0,
y
: 0,
name
: "Origin",
};
logPoint
(
obj
);
logName
(
obj
);
Try

TypeScript 的类型系统是结构的,而不是名义的:我们可以将 obj 用作 Pointlike,因为它具有 xy 属性,且都是数字。 类型之间的关系由它们包含的属性决定,而不是它们是否以某种特定关系声明。

TypeScript 的类型系统也不是具象化的:在运行时没有任何东西会告诉我们 objPointlike。 事实上,Pointlike 类型在运行时不以任何形式存在。

回到作为集合的类型的想法,我们可以将 obj 视为同时属于 Pointlike 值集合和 Named 值集合的成员。

结构类型的结果

面向对象编程的程序员经常对结构类型的两个特定方面感到惊讶。

空类型

第一个是空类型似乎违背了预期:

ts
class 
Empty
{}
function
fn
(
arg
:
Empty
) {
// 做点什么? } // 没有错误,但这并不是一个 'Empty' ?
fn
({
k
: 10 });
Try

TypeScript 通过查看提供的参数是否为有效的 Empty 来确定这里的 fn 调用是否有效。 它通过检查 { k: 10 }class Empty { }结构来实现。 我们可以看到 { k: 10 } 拥有 Empty 所拥有的所有属性,因为 Empty 没有属性。 因此,这是一个有效的调用!

这可能看起来令人惊讶,但最终这与名义型面向对象编程语言中强制执行的关系非常相似。 子类不能删除其基类的属性,因为这样做会破坏派生类与其基类之间的自然子类型关系。 结构类型系统只是通过根据具有兼容类型的属性来描述子类型,隐式地识别了这种关系。

相同的类型

另一个常见的困惑来源来自相同的类型:

ts
class Car {
  drive() {
    // 踩油门
  }
}
class Golfer {
  drive() {
    // 把球打远
  }
}

// 没有错误?
let w: Car = new Golfer();

同样,这不是错误,因为这些类的结构是相同的。 虽然这似乎可能成为混淆的根源,但在实践中,不应该相关的相同类并不常见。

我们将在“类”一章中进一步了解类之间如何相互关联。

反射

面向对象编程的程序员习惯于能够查询任何值的类型,即使是泛型值:

csharp
// C#
static void LogType<T>() {
    Console.WriteLine(typeof(T).Name);
}

因为 TypeScript 的类型系统被完全擦除,关于例如泛型类型参数实例化的信息在运行时不可用。

JavaScript 确实有一些有限的原语,如 typeofinstanceof,但请记住,这些操作符仍然作用于类型擦除输出代码中存在的值。 例如,typeof (new Car()) 将是 "object",而不是 Car"Car"

下一步

这是对日常 TypeScript 中使用的语法和工具的简要概述。从这里开始,你可以: