让我们先看两段代码。
Java 代码:
CountPointsTransactDto record = new CountPointsTransactDto();
record.amount = 100;
TypeScript 代码:
const record = new CountPointsTransactDto();
record.amount = 100;
乍一看,这简直就是双胞胎。语法结构、关键字、甚至赋值方式都如出一辙。很多从 Java 转过来的后端同学看到这里会松一口气:"切,TS 不就是带类型的 JS 嘛,跟写 Java 没区别。"
大错特错。
虽然它们长得像,但在计算机内存的微观世界里,这两行代码触发的逻辑完全属于两个不同的宇宙。
🧊 一、内存模型:蓝图 vs 黏土
1. Java 的 new:严格的蓝图 (Blueprint)
在 Java 中,类(Class)是一张不可修改的工程蓝图。
当你执行 new 时,JVM 会做以下事情:
-
加载蓝图 :读取
.class文件,解析字段和方法。 -
划地盘:根据蓝图计算出对象需要多少内存(例如:2个 int + 1个 String 引用 = 固定字节数)。
-
浇筑 :在堆内存中开辟一块固定大小、固定结构的区域。
结论 :Java 对象出生那一刻,它的结构就定死了。你不可能在运行时突然给它加一个 nickname 属性。如果你敢这么做,编译器会直接报错,IDE 会标红。
2. TypeScript (JS) 的 new:可塑的黏土 (Clay)
在 TypeScript(最终运行的是 JavaScript)中,类只是一个函数 ,对象只是一个哈希表(Key-Value Map)。
当你执行 new 时,JS 引擎做了这 4 件事:
-
创建一个空的哈希表
{}。 -
把这个空表的
__proto__指针指向类的prototype(为了能用类的方法)。 -
执行构造函数(Constructor),给这个哈希表塞入初始属性(如
this.amount = 0)。 -
返回这个哈希表。
结论:JS 对象本质上是一团可以随意揉捏的黏土。
虽然 TypeScript 的编译器(tsc)会像 Java 一样检查你的拼写,但在运行时 ,你完全可以给这个对象追加任何属性(record.whatever = 123),JS 引擎绝不会拦你。
⚔️ 二、赋值逻辑:权限控制 vs 约定俗成
Java:严防死守
Java 能不能直接赋值,取决于访问修饰符。
-
如果
amount是public,可以。 -
但 Java 开发的黄金法则是 封装(Encapsulation) 。绝大多数 entity/dto 的字段都是
private的,必须通过setAmount()方法来访问。 -
为什么? 为了安全。Java 可以在 setter 里加逻辑(比如
if (amount < 0) throw error),保证数据安全。
TypeScript:自由奔放
TypeScript 默认所有属性都是 public。
在 TS/JS 生态中,直接操作属性(record.amount = 100)是标准做法。
-
我们很少在 DTO 里写
getAmount()/setAmount()。 -
为什么? 因为 JS 追求灵活性和简洁。如果真要控制权限,TS 也有
private关键字,但那只是编译时的约束,编译成 JS 后,私有属性依然可以被访问(虽然不推荐)。
🦆 三、类型系统:名义 vs 结构 (核心差异)
这是最颠覆 Java 开发者认知的一点。
Java:名义类型 (Nominal Typing)
Java 只认名字(身份证)。
哪怕两个类长得一模一样,名字不一样,就是不兼容。
class A { int x; }
class B { int x; }
A obj = new B(); // ❌ 报错!B 不是 A。
TypeScript:结构化类型 (Structural / Duck Typing)
TypeScript 只认长相(鸭子测试)。
只要你长得像(属性列表匹配),你就是它。
class A { x: number; }
class B { x: number; }
const obj: A = new B(); // ✅ 通过!因为 B 也有 x,结构满足 A 的要求。
这也是为什么在 TS 里,你经常看到有人偷懒 ,不用 new,而是直接写个字面量对象:
// TS 允许这样(只要属性对得上)
const record: CountPointsTransactDto = { amount: 100 };
🤔 四、灵魂拷问:既然如此,为什么还要用 new?
既然 { amount: 100 } 就能冒充 DTO,为什么我们在 NestJS 中还是推荐写:
const record = new CountPointsTransactDto();
原因有三:
-
初始值 (Default Values):
类里定义了
status = 'PENDING',new出来的对象自动就有。字面量{}必须手动写一遍。 -
方法 (Methods):
只有
new出来的实例才挂载了原型链,才能调用 DTO 里定义的isValid()或calculateTax()方法。 -
元数据 (Metadata & Decorators):
这是最重要的。NestJS 大量使用装饰器(如
@IsString(),@Expose())。纯 JSON 对象是不带这些装饰器信息的 。只有通过
class-transformer的plainToInstance或者直接new出来的对象,验证管道(ValidationPipe)才能正常工作。
📝 总结
| 特性 | Java (new) | TypeScript / JS (new) |
|---|---|---|
| 本质 | 按照蓝图开辟固定内存块 | 创建空哈希表,链接原型链 |
| 结构灵活性 | 不可变 (编译后固定) | 高度可变 (运行时可增删属性) |
| 属性赋值 | 依赖 public/private,常用 Setter | 默认 public,常用直接赋值 |
| 类型兼容 | 看名字 (必须是同一个类或子类) | 看结构 (属性匹配即可) |
| 使用建议 | 必须用 new |
推荐用 new (为了默认值和装饰器) |
一句话总结:
不要被 TypeScript 的语法糖欺骗了。它虽然穿上了 Java 的西装(Class, new, private),但它的灵魂依然是那个自由、灵活、基于原型的 JavaScript。理解了这一点,你写出的 TS 代码才会有真正的"TS 味"。