如何用ES6 Proxy检测JavaScript类任意私有成员的变更?
问题
我正在使用JavaScript Canvas API构建一个2D渲染器,尝试通过在可渲染对象状态无变更时跳过渲染来提升性能。
为此,我希望构建一个可扩展的类(命名为Watchable),它能检测自身状态变化,同时让子类无需感知该状态追踪逻辑。客户端代码可继承该类创建可渲染对象,再将这些对象注册到渲染器中。
经过研究,我用Proxy实现了一个部分可行的方案,编写了如下makeWatchable函数:
function makeWatchable(obj) { // Local dirty flag in the proxy's closure, but protected from // external access let dirty = true; return new Proxy(obj, { // I need to define accessors and mutators for the dirty // flag that can be called on the proxy. get(target, property) { // Define the markDirty() method if(property === "markDirty") { return () => { dirty = true; }; } // Define the markClean() method if(property === "markClean") { return () => { dirty = false; }; } // Define the isDirty() method if(property === "isDirty") { return () => { return dirty; }; } return Reflect.get(target, property); }, // A setter trap to set the dirty flag when a member // variable is altered set(target, property, value) { // Adding or redefining functions does not constitute // a state change if( (target[property] && typeof target[property] !== 'function') || typeof value !== 'function' ) { if(target[property] !== value) dirty = true; } target[property] = value; return true; } }); }
该函数接收一个对象并返回一个Proxy,通过捕获对象的setter来设置Proxy闭包内的dirty标记,并为该标记定义了访问器和修改器。
随后我用这个函数创建了Watchable基类:
// A superclass for watchable objects class Watchable { constructor() { // Simulates an abstract class if (new.target === Watchable) { throw new Error( 'Watchable is an abstract class and cannot be instantiated directly' ); } return makeWatchable(this); } }
子类可继承Watchable以获得状态追踪能力。
但该方案存在两个明显局限:
Watchable仅能检测状态的浅层变更。Watchable仅能检测公共状态的变更。
第一个局限我可以交由客户端代码处理,但我希望在无需子类额外操作的前提下,支持追踪Watchable子类中的任意私有成员。
例如,我希望定义如下子类:
class SecretRenderable extends Watchable { #secret; constructor() { super(); this.#secret = 'Shh!'; } setSecret(newSecret) { this.#secret = newSecret; } }
并按如下方式使用:
const obj = new SecretRenderable(); obj.markClean(); obj.setSecret('SHHHHHHHHH!!!!!');
此时obj.isDirty()应返回true。
我原本期望this.#secret = newSecret这行代码会被Proxy的set陷阱捕获,但实际上它完全绕过了该陷阱。
请问能否修改我的Proxy实现以实现私有状态变更检测?如果不能,我应考虑哪些替代方案?
解决方案
为什么Proxy无法捕获私有成员变更
JavaScript的私有字段(#xxx)是完全绕过对象的常规属性访问机制的。Proxy的set/get陷阱只能拦截通过obj.prop或Reflect.set/get进行的公共属性访问,而私有字段的读写是直接在类的内部作用域中完成的,不会经过Proxy的任何陷阱,所以你的Proxy方案无法检测私有成员的变更。
可行的替代方案
1. 基类提供手动标记能力(最低成本)
保留markDirty方法,要求子类在修改私有成员时主动调用。虽然需要子类配合,但实现最简单:
class SecretRenderable extends Watchable { #secret; constructor() { super(); this.#secret = 'Shh!'; } setSecret(newSecret) { if (this.#secret !== newSecret) { this.#secret = newSecret; this.markDirty(); // 主动标记脏状态 } } }
2. 用装饰器自动注入标记逻辑
使用类/方法装饰器,自动为修改私有成员的方法添加markDirty调用,子类无需手动编写标记代码:
// 定义装饰器 function trackChanges(target, propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args) { const result = originalMethod.apply(this, args); this.markDirty(); return result; }; return descriptor; } // 子类使用装饰器 class SecretRenderable extends Watchable { #secret; constructor() { super(); this.#secret = 'Shh!'; } @trackChanges setSecret(newSecret) { this.#secret = newSecret; } }
注意:装饰器目前是Stage 3提案,需要Babel/TypeScript编译或在最新浏览器中启用相关特性。
3. 用访问器封装私有成员
将私有字段封装在公共getter/setter中,让Proxy拦截访问器调用:
class SecretRenderable extends Watchable { #secret; constructor() { super(); this.#secret = 'Shh!'; } get secret() { return this.#secret; } set secret(newSecret) { this.#secret = newSecret; } setSecret(newSecret) { this.secret = newSecret; // 通过公共setter修改,会被Proxy捕获 } }
这种方式下,Proxy的set陷阱会自动标记脏状态,子类只需通过访问器操作私有成员即可。
4. WeakMap存储私有状态(备选,不推荐)
用WeakMap在类外部存储私有状态,让所有读写通过公共逻辑触发Proxy拦截,但会失去私有字段的真正封装性:
const secretMap = new WeakMap(); class SecretRenderable extends Watchable { constructor() { super(); secretMap.set(this, 'Shh!'); } setSecret(newSecret) { const current = secretMap.get(this); if (current !== newSecret) { secretMap.set(this, newSecret); this.markDirty(); } } }
总结
Proxy无法拦截私有字段的变更,这是JavaScript私有字段的设计特性导致的。最符合你需求的方案是使用装饰器自动注入标记逻辑,或者用访问器封装私有成员,前者能让子类几乎无需感知追踪逻辑,后者需要子类遵循一定编码规范。
内容的提问来源于stack exchange,提问作者Banshee

