Trait Views:在 JavaScript 中无需继承即可暴露行为
Source: Dev.to
介绍
JavaScript 为我们提供了多种共享行为的方式:继承、mixins、组合、接口(通过 TypeScript)。
然而在更大的系统中,它们往往在某些方面不足。
- 继承 过于僵硬。
- Mixins 往往会泄漏状态。
- 直接传递原始对象会暴露过多的表面。
JavaScript 缺少一种表达方式:
“给我这个对象的 视图,只暴露特定的能力,并带有默认行为——而不改变对象本身。”
本文介绍 Trait Views:一种受 Rust traits 启发、适配 JavaScript 对象模型的运行时模式。
这不是语言提案,也不是对现有模式的替代。它是对一种缺失抽象的探索。
问题
考虑一个简单的情形:你有一个对象,想把它视为 observable(可观察的)。
- 不是通过继承自
Observable基类。 - 不是通过向其混入方法。
- 不是通过直接传递对象本身并相信所有人都会“正确使用”。
你想说:
“对于系统的这部分,这个对象应当仅被视为可观察的。”
JavaScript 并未提供原生方式来表达这一点。
Trait Views — 概念
Trait View 是一种派生对象,用于公开另一个对象的特定行为。它是:
- not 原始对象
- not 它的副本
- not 应用于它的 mixin
它是一个 projection。
你并没有 add 一个 trait 到对象上。
你 derive a view 该对象。
一个最小示例:Observable
class Observable {
static from = trait(Observable)
observe(): number {
return 0
}
// Note this one is optional
observeTwice?(): number {
return this.observe() * 2
}
}
该 trait 定义了:
- 一个核心行为:
observe - 一个基于核心行为的默认行为:
observeTwice
现在,来看一个完全无关的对象:
class Sensor implements Observable {
value = 21
observe(): number {
return this.value
}
}
implements Observable 子句很重要。即使 Observable 从未被扩展,TypeScript 仍然会强制检查:
- 所有必需的方法都已实现
- 方法签名兼容
- 重构仍保持类型安全
这意味着 Trait Views 并不是“暗箱里的鸭子类型”。它们是 结构化类型并在编译时进行检查。
const sensor = new Sensor()
Observable.from(sensor).observeTwice() // 42
这里发生的事情微妙但重要。
Sensor本身保持不变。- 没有方法被复制到它上面。
- 没有引入继承关系。
相反,Observable.from(sensor) 创建了一个 强类型视图,该视图暴露了可观察的行为,包括原始对象从未拥有的默认逻辑。原始对象本身并不可观察;视图 才是。
Source: …
无状态 vs 有状态 Trait 视图
Trait 视图可以以两种模式存在。
无状态视图 Observable.from(...) → Stateless
无状态的 trait 视图:
- 不拥有任何状态
- 它自己的构造函数刻意未使用且从不被调用
- 按实例进行缓存,并为保持稳定性而被冻结
- 将所有行为委托回原始对象
从 TypeScript 的角度来看:
- 所有 trait 方法都保证存在
- trait 拥有的属性保持 可选
在无状态模式下,trait 拥有的属性被刻意声明为可选。这是为了预见实现对象可能会暴露同名的 getter,在这种情况下实例化 trait 状态将是不正确的。
概念上,这类似于 Rust 中的借用 trait 对象 (&dyn Trait):在已有状态之上提供一个稳定的接口。
有状态视图 Observable.from(...) → Stateful
有状态的 trait 视图:
- 拥有自己的内部状态
- 拥有自己的构造函数和参数
- 通过
Observable.from(object, param1, param2, …)显式构造 - 不进行缓存
从类型的角度来看:
- 所有 trait 方法都存在
- 所有 trait 属性都保证出现
这使得 trait 能在不污染原始对象的前提下携带状态。
两种模式共存是因为它们解决了不同的问题。可以通过选项为特定 trait 选择模式:
class MyTrait {
static from = trait(MyTrait, { stateful: true });
public myState?: number = 42;
constructor(myParameter: number) {}
}
Trait 视图作为能力边界
到目前为止,Trait 视图看起来像是一种共享行为的方式。
但它们也 减少了表面面积。
class Disposable {
static from = trait(Disposable)
dispose(): void {
console.log("default dispose")
}
}
class Resource implements Disposable {
secret = "do not touch"
disposed = false
dispose() {
this.disposed = true
}
dangerousOperation() {
console.log(this.secret)
}
}
const resource = new Resource()
const disposable = Disposable.from(resource)
disposable.dispose() // OK
disposable.dangerousOperation() // ❌ not accessible
Trait 视图只暴露 一种能力:释放。
原始对象可能拥有许多方法、许多状态、许多不变式——但视图有意限制了可见的内容。与其传递整个对象,不如传递 它们被允许执行的操作。
Trait 视图不仅仅是为了复用。它们是关于 通过投影实现封装。
Source: …
工作原理(概念层面)
在高层次上,特质视图:
- 使用特质原型进行方法查找
- 将调用转发给底层目标对象(无状态)或转发给其内部状态(有状态)
- 通过
trait(...)生成的静态from辅助函数创建 - 当为无状态时,可按目标进行缓存,以确保身份保持
(实现细节已省略;重点在可观察的 API 和类型层面的保证。)
默认行为
- 将被覆盖的方法绑定到原始对象
- 可选地绑定访问器
- 缓存无状态视图(采用弱引用,以免阻止垃圾回收)
- 为了稳定性,冻结无状态视图
原始对象从不被修改。
特质视图是一个独立的对象,具有明确的表面。
与 Rust Trait 的比较
Trait Views 受 Rust trait 启发,但它们并不相同。
相似之处
- 面向行为的抽象
- 默认方法
- 通过稳定接口进行动态分发
差异
- 解析在运行时进行,而非编译时
- 没有一致性或孤儿规则
- 重写基于名称
- TypeScript 无法表达所有保证
这不是模式的缺陷,而是 JavaScript 动态特性的结果。
Trait Views 旨在提供 相似的易用性,而非完全相同的语义。
为什么不直接绑定函数?
乍一看,Trait View 可能显得微不足道。毕竟,人们完全可以创建一个新对象并手动绑定几个方法。
区别不在于运行时发生了什么——而在于所建模的内容。
Trait View 提供:
- 一致的抽象
- 对象状态与特性行为之间的清晰边界
- 稳定的、带类型的接口
- 可选的缓存和冻结保证
- 跨代码库共享的思维模型
手动绑定函数只能解决局部问题。
Trait View 旨在解决系统性问题。
它们更关注表达意图,而不仅仅是便利性。
与现有库的比较
已有多个库在 JavaScript 和 TypeScript 中探索 trait。Trait Views 并不是要取代它们——它们的关注点不同。
@traits-ts/core
- 提供一种 trait(或 mixin)机制,以在类中扩展多种基础功能。
- 利用 TypeScript 的类型系统和常规的
class extends机制。 - 将 trait 行为组合成新的类层次结构,具备编译时类型安全。
使用场景: 在类型定义阶段已知并应用 trait 的静态组合。
Trait Views 关注 运行时适配 和 对现有对象的每实例视图,而不是静态组合。
traits.js
- 专门用于 trait 组合的库,基于经典的可复用行为单元。
- 允许将零个或多个 trait 组合成单一的复合 trait,然后使用该组合行为创建对象。
使用场景: 在构造时创建具有组合行为的新对象。
Trait Views 则持相反的视角:它们 从 trait 派生对象,在 不修改原对象 的前提下为现有对象创建视图。
权衡与限制
- Trait Views 依赖运行时反射。
- 它们假设 getter 没有副作用。
- 它们无法阻止所有形式的 monkey‑patching(猴子补丁)。
- 它们要求在 API 设计上保持纪律。
这种模式并不适用于所有情况。Trait Views 特别适用于:
- 引擎和仿真
- ECS 风格的架构
- 基于能力的 API 或者对表面控制重要的系统
它们 不 适合用于简单的 CRUD 应用或 UI 密集的代码库。
结论 — 以及一个未解之问
Trait Views 不是一种新的语言特性。它们是一种 模式 —— 一种以不同方式思考 JavaScript 中行为的方式。
它们介于以下之间:
- 类 Rust 的 trait
- 基于能力的设计
- 运行时对象视图
在目前阶段,Trait Views 仍是一个 实验。尚未有公开的库——只有一个想法、一个实现以及一系列权衡。
如果这与你产生共鸣
- 作为用户
- 作为库作者
- 作为关心语言设计的人
那么反馈很重要。
也许你已经以不同的方式解决了类似的问题,并愿意分享你的方法。
- 这应该保持为一种模式吗?
- 它应该成为一个小型实验库吗?
- 还是应该作为专用系统的内部工具?
我真诚地想了解社区的看法。