Trait Views:在 JavaScript 中无需继承即可暴露行为

发布: (2026年1月15日 GMT+8 02:06)
12 min read
原文: Dev.to

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 仍是一个 实验。尚未有公开的库——只有一个想法、一个实现以及一系列权衡。

如果这与你产生共鸣

  • 作为用户
  • 作为库作者
  • 作为关心语言设计的人

那么反馈很重要。

也许你已经以不同的方式解决了类似的问题,并愿意分享你的方法。

  • 这应该保持为一种模式吗?
  • 它应该成为一个小型实验库吗?
  • 还是应该作为专用系统的内部工具?

我真诚地想了解社区的看法。

Back to Blog

相关文章

阅读更多 »

JavaScript的秘密生活:蓝图

ES6 类只是原型的“语法糖”。Timothy 站在黑板前,欣赏自己的作品。他画了一个完美的矩形框。> “终于,”...