You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 17:17:02