- 文档
- 教程
【免费下载链接】typescript-book-chinese
TypeScript Deep Dive 中文版
本文基于《深入理解 TypeScript》(typescript-book-chinese)tips 章节中的《减少 setter 属性的使用》展开。该文讨论的是一个非常实际且常被忽视的类设计问题:在 TypeScript(JavaScript)的class中,setter/getter(存取器)与显式的setBar(value)/getBar()方法该如何取舍。读完本文,你将理解为什么面向复合赋值的 setter 会损害代码可读性、显式方法在可维护性上有哪些具体优势,以及什么场景下才值得继续使用存取器,并掌握与仓库内「函数参数」「对象字面量惰性初始化」等技巧配套的落地方案。
问题场景:一行赋值背后藏着的副作用
先看原文档给出的核心示例。假设业务代码中出现了这样一段赋值:
foo.bar = { a: 123, b: 456 };而Foo类的内部实现长这样:
class Foo { a: number; b: number; set bar(value: { a: number; b: number }) { this.a = value.a; this.b = value.b; } } let foo = new Foo();第一眼看上去,foo.bar = {...}只是「把属性bar赋了个新值」,简单、无害。但事实并非如此:由于Foo定义了set bar,这一行赋值实际触发了 setter 内部逻辑,会同时修改foo的a和b两个字段。
原文档对此的结论非常明确:这并不是 setter 的一个好的使用场景。当开发人员阅读第一段代码时,他并不知道将要被更改的所有内容的上下文。相反,如果开发者写成foo.setBar(value),函数调用本身的语义就会提醒读者:foo内部可能发生一些改变。
这正是「减少 setter 属性的使用」这一技巧的核心——让“会改变内部状态”这个事实在调用点变得可见。
存取器语法基础:setter 与 getter 在 TypeScript 中如何工作
要深入讨论取舍,先明确 TypeScript 中的存取器语法。TypeScript 的class原生支持 ES5 的 accessor 语法,通过get/set关键字声明:
class Foo { private _bar: { a: number; b: number }; // 取值器:读取时被调用 get bar() { return this._bar; } // 赋值器:赋值时被调用 set bar(value: { a: number; b: number }) { this._bar = value; } }要点如下:
get与set共用同一个属性名(这里是bar),二者组合后,对外表现为一个普通属性;- 只声明
get时,属性是只读的(赋值会报错); - 只声明
set时,属性是只写的(读取会报错); set bar(value: T)的参数类型即属性可被赋予的类型,value参数由赋值运算符右侧的值隐式传入,调用方无法命名它,也无法额外传参;- 存取器的类型注解需要满足 TypeScript 的类型兼容性规则,赋值类型必须能被 setter 参数类型接收。
从上面的例子可以直观看出问题:通过foo.bar = {...}赋值时,赋值的目标是属性名,而不是方法名。读代码的人看到的是一处属性写入,但实际执行的却是一段可能包含任意逻辑的函数体。
为什么显式方法更优:四层可读性分析
原文档认为foo.setBar(value)优于foo.bar = value,我们可以把这背后的理由展开为四个维度:
1. 调用点语义清晰
foo.bar = value是一个「赋值语句」,语义上是纯数据写入;而foo.setBar(value)是一个「方法调用」,语义上天然暗示「可能触发内部逻辑」。正如原文档所说,开发者在使用foo.setBar(value)时,会意识到在foo里可能会引起一些改变,于是会更谨慎地审查副作用。
2. 复合赋值与副作用透明化
原文档示例中,setter 把一次赋值拆解为对a、b两个字段的写入——这属于典型的复合赋值。此外 setter 还经常被用来承载校验、归一化、事件通知等逻辑:
class Foo { set bar(value: { a: number; b: number }) { if (value.a < 0) { throw new Error('a 不能为负数'); } this.a = value.a; this.b = value.b; this.onChanged(); // 触发内部通知 } }校验和副作用本身没有错,但当它们被藏进=赋值背后时,调用方完全无感。改用显式方法后:
class Foo { setBar(value: { a: number; b: number }) { if (value.a < 0) { throw new Error('a 不能为负数'); } this.a = value.a; this.b = value.b; this.onChanged(); } }foo.setBar(...)让读者(以及 IDE 的调用链分析、断点调试)能顺着方法名找到逻辑入口。
3. 可调试与可测试性
- 调试:在
set bar(...)中打断点,只有在赋值语句执行时才会命中,且无法从调用栈上区分是「哪一次赋值」;而setBar的名字与调用点一一对应,日志与堆栈信息更可读。 - 测试:显式方法可以像普通方法一样被单独测试、被 mock;而存取器需要借助「赋值」来触发,测试意图不明显。
4. 与函数式风格的一致性
本仓库 docs/tips/functionParameters.md 讨论了另一个相关技巧:当函数参数过多或类型相同时,建议改为接收一个对象参数foo({ flagA, flagB }),因为调用形式的可读性决定了错误能否被及时发现。显式方法正是把这一思想延伸到类成员上——参数以对象形式传入,方法名表达意图,类型注解保证安全:
class Foo { setBar(config: { a: number; b: number }) { this.a = config.a; this.b = config.b; } }而 getter 侧同理,显式的getBar()比foo.bar更清楚地表达「这是一个可能触发计算/缓存逻辑的读取操作」。
setter 的适用边界:什么时候可以保留
「减少 setter 属性的使用」不等于「禁用 setter」。结合原文档的观点,可以总结出如下取舍标准:
| 场景 | 建议 | 理由 |
|---|---|---|
| setter 仅做简单的一对一赋值(纯数据封装) | 可以保留 | 无副作用,读起来仍是「属性赋值」的直觉 |
| setter 内部涉及复合字段写入、校验、换算、通知 | 改用显式方法 | 副作用需要被调用点看见 |
| getter 是纯计算、无副作用 | 可以保留 | 例如get fullName() { return this.first + this.last; } |
| getter 内部有缓存、异步或昂贵计算 | 改用显式方法 | 读取语义会误导调用方对性能的预期 |
判断标准可以浓缩为一句话:如果get/set内部只是读取或写入「同一个字段」,存取器是合适的;只要内部逻辑超出了“同名同字段”的范畴,就应显式化。
另外要注意,存取器与字段同名时容易引起无限递归(典型的错误写法是在 getter 内直接返回同名属性)。把内部数据放在下划线私有字段(_bar)上是常见规避方式,但如果逻辑一复杂,显式方法显然更省心。
配套实践:来自本仓库 tips 章节的关联建议
「减少 setter 属性」并不是孤立的一条编码规范,它与本仓库 tips 章节的多篇文章互相呼应:
- 对象字面量的惰性初始化(docs/tips/lazyObjectLiteralInitialization.md):
let foo = {}; foo.bar = 123;这种「先建空对象、后补属性」的写法在 TypeScript 中会因类型推断报错。它从另一个侧面说明:逐个补属性的赋值式编程本身就是脆弱的,更稳妥的做法是构造时就一次性提供完整结构。这恰好与「不要用 setter 拆字段赋值」一脉相承——数据结构的完整性应当在入口处保证。 - 函数参数对象化(docs/tips/functionParameters.md):参数过多时改为接收对象
foo({ flagA, flagB }),与本文setBar({ a, b })的推荐写法完全同构,两者都在强调「调用形式的可读性」。 - 类是有用的(docs/tips/classAreUseful.md):该文建议用
class组织内部状态与初始化逻辑。一旦选择 class 承载状态,成员对外暴露的方式(方法 vs 存取器)就决定了这类状态的可控性——显式方法让状态变更路径更可追踪。 - 静态构造函数(docs/tips/staticConstructors.md):该文展示了用
MyClass.initalize()显式触发初始化。这与本文的结论一致:在 TypeScript/JavaScript 中,凡是会「改变对象内部状态」的动作,用命名方法显式表达,比依赖隐式语法钩子更符合可维护性直觉。
总结:把「可能引起改变」写进调用点
回到原文档的核心主张,本文的全部讨论可以收敛为一句实践准则:
倾向于使用更精确的
set/get函数(如setBar、getBar),减少使用setter/getter。
具体落地时,建议在团队代码规范中明确:
- setter/getter 仅用于无副作用的简单属性封装;
- 涉及复合字段、校验、换算、通知等逻辑时,一律使用
setBar(value)形式的显式方法; - 对外暴露状态变更的 API,优先设计为方法而非赋值,让「可能引起内部改变」的信号出现在调用点;
- 结合 函数参数对象化 的做法,为多字段更新提供结构化的参数对象,兼顾类型安全与可读性。
这样,当后来者(包括未来的你自己)读到foo.setBar({ a: 123, b: 456 })时,不需要翻看类的实现,就已经知道:这一次调用,可能会改变foo内部的某些东西。而这,正是原文档想传达的最重要的编码直觉。
- 文档
- 教程
【免费下载链接】typescript-book-chinese
TypeScript Deep Dive 中文版
相关推荐
限制 TypeScript 属性 setter 的使用:优先显式 setBar/getBar 函数的设计实践
限制 TypeScript 属性 setter 的使用:优先显式 setBar/getBar 函数的设计实践 本文基于 TypeScript Deep Dive
教程深入理解torchdiffeq中的显式与隐式Adams方法:线性多步法的完整实现指南
深入理解torchdiffeq中的显式与隐式Adams方法:线性多步法的完整实现指南 torchdiffeq是一个强大的PyTorch微分方程求解库,专门用于解
深度学习科学计算使用显式组件变体替代布尔属性:next-shadcn-dashboard-starter 中的 React 组合模式实践
使用显式组件变体替代布尔属性:next shadcn dashboard starter 中的 React 组合模式实践 在复杂 React 组件中, isTh
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考