如何基于ES6 Proxy实现保留原型的不可变类实例?
Hey there! Let's work through creating an immutable class instance with ES6 Proxy—your core idea is solid, but there are a few key fixes and improvements needed to make it behave as expected, especially around preserving the class prototype and handling property updates correctly.
First, let's break down the issues with your current code:
- The
settrap in a Proxy must return a boolean (to signal if the assignment succeeded), not the modified object. Returning an object here will throw a runtime error. - JavaScript's assignment expression (
obj.prop = value) evaluates tovalue, not the modified object. So your linelet newJesonProxy = (jesonProxy.schema = {})would setnewJesonProxyto the new schema object, not the updated Proxy instance. - We need to ensure deep cloning preserves the original class's prototype chain (so the cloned instance is still a
Menobject).
Solution 1: Functional Update Method (Recommended)
This approach uses a custom set method on the Proxy to handle updates, ensuring we get a new immutable Proxy instance every time we modify a property. It's clear, avoids JavaScript syntax limitations, and preserves the class prototype.
// First, a deep clone function that preserves prototype chains // (or use lodash's _.cloneDeep which does this automatically) function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map(deepClone); // Create a new instance of the original object's constructor const cloned = new obj.constructor(); for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } function createImmutableProxy(instance) { const handler = { get(target, key) { // Expose a custom set method to generate updated instances if (key === 'set') { return (prop, value) => { // Deep clone the original instance (preserves prototype) const clonedInstance = deepClone(target); // Apply the update (works with both direct properties and setters) clonedInstance[prop] = value; // Return a new immutable Proxy for the updated instance return createImmutableProxy(clonedInstance); }; } // Return regular properties as-is return Reflect.get(target, key); }, // Block direct assignment to enforce using the set method set(target, key) { throw new Error(`Cannot mutate immutable instance directly. Use .set('${key}', value) instead.`); } }; return new Proxy(instance, handler); } // Your original Men class class Men { constructor(name) { this.schema = { prop: { innerProp: {} } }; this.name = name; } set newName(name) { this.name = name; } } // Usage example const jeson = new Men("jeson"); const jesonProxy = createImmutableProxy(jeson); // Update schema to get a new immutable instance const newJesonProxy = jesonProxy.set('schema', { newProp: { newInnerProp: {} } }); console.log(newJesonProxy.schema); // { newProp: { newInnerProp: {} } } console.log(jesonProxy.schema); // Original value remains unchanged: { prop: { innerProp: {} } } // Test the class setter const updatedNameJeson = jesonProxy.set('newName', 'Jeson Updated'); console.log(updatedNameJeson.name); // "Jeson Updated" // Verify prototype is preserved console.log(newJesonProxy instanceof Men); // true
Key Features:
- Immutable Updates: Every modification returns a new Proxy instance, leaving the original untouched.
- Prototype Preservation: The deep clone function creates a new instance of the original class, so
instanceofchecks still work. - Supports Setters: The custom
setmethod works with class setters (likenewName) just like regular properties. - Prevents Accidental Mutation: Direct assignment throws an error, so you can't accidentally modify the original instance.
If You Prefer Assignment Syntax (Workaround)
JavaScript's assignment syntax doesn't let us return the modified object as the result of the expression, but we can use a Proxy trap to create a new instance and expose it via a separate property. Note this is less intuitive than the functional approach:
function createImmutableProxy(instance) { let latestInstance = instance; const handler = { set(target, key, value) { const cloned = deepClone(target); cloned[key] = value; latestInstance = cloned; return true; }, get(target, key) { if (key === 'getLatest') { return () => createImmutableProxy(latestInstance); } return Reflect.get(target, key); } }; return new Proxy(instance, handler); } // Usage const jesonProxy = createImmutableProxy(new Men("jeson")); jesonProxy.schema = { newProp: {} }; const newJesonProxy = jesonProxy.getLatest();
But this approach has downsides: it relies on a side effect (updating latestInstance) and can lead to confusion if multiple updates are made in quick succession. The functional method is far more predictable.
内容的提问来源于stack exchange,提问作者Goor Lavi

