news 2026/10/26 10:59:58

深入理解 TypeScript 编码实践:减少 setter 属性的使用,以显式方法替代隐式赋值

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解 TypeScript 编码实践:减少 setter 属性的使用,以显式方法替代隐式赋值
  • 文档
  • 教程

【免费下载链接】typescript-book-chinese

TypeScript Deep Dive 中文版

项目地址:https://gitcode.com/gh_mirrors/ty/typescript-book-chinese
点击查看免费下载

本文基于《深入理解 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。

具体落地时,建议在团队代码规范中明确:

  1. setter/getter 仅用于无副作用的简单属性封装;
  2. 涉及复合字段、校验、换算、通知等逻辑时,一律使用setBar(value)形式的显式方法;
  3. 对外暴露状态变更的 API,优先设计为方法而非赋值,让「可能引起内部改变」的信号出现在调用点;
  4. 结合 函数参数对象化 的做法,为多字段更新提供结构化的参数对象,兼顾类型安全与可读性。

这样,当后来者(包括未来的你自己)读到foo.setBar({ a: 123, b: 456 })时,不需要翻看类的实现,就已经知道:这一次调用,可能会改变foo内部的某些东西。而这,正是原文档想传达的最重要的编码直觉。

  • 文档
  • 教程

【免费下载链接】typescript-book-chinese

TypeScript Deep Dive 中文版

项目地址:https://gitcode.com/gh_mirrors/ty/typescript-book-chinese
点击查看免费下载
上一篇:揭秘Barlow字体设计:低对比度与微圆角如何提升可读性?
下一篇:OpenResume表单状态恢复:用户会话与历史记录

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 5:18:13

TortoiseGit连接被拒?从TCP握手到SSH配置的排查指南

用 TortoiseGit 拉代码或推代码的时候&#xff0c;弹出一个红色报错框&#xff1a;“Network error: Connection refused”。这个场景我见过太多次了&#xff0c;说真的&#xff0c;第一次遇到时我也懵过——感觉网络明明是通的&#xff0c;浏览器也能打开网页&#xff0c;怎么…

作者头像 李华
网站建设 2026/10/25 8:16:09

终极指南:RAD Debugger开发环境搭建详细步骤

终极指南&#xff1a;RAD Debugger开发环境搭建详细步骤 【免费下载链接】raddebugger A native, user-mode, multi-process, graphical debugger. 项目地址: https://gitcode.com/GitHub_Trending/ra/raddebugger 想要快速搭建RAD Debugger开发环境吗&#xff1f;作为一…

作者头像 李华