read

Proxy & Reflect

This article will show the basics to building a Proxy object that traps some standard object behavior, and implements some custom logic either in addition to, or in place of, the default behavior. It utilizes the Reflect object to perform the default behavior when applicable.

I'll trap the set, and deleteProperty operations in order to have two different states for an object.

  1. An internal state that reflects values that are persisted, in theory, to disk.
  2. A current, or transient, state that may or may not be equal to internal state.

First, I create an object named Modela that, upon initialization, returns a proxied object that has the following properties.

  1. modifiedProperties is an array that will hold the names of any properties that have been modified from their internal state.
  2. __instance__ returns the internal object state.
  3. save method will move property values from the transient state to the internal state.

The proxy traps deleteProperty to override its default behavior.

  1. Determines if the property being deleted is on the internal model object.
  2. If it is an internal model property, add to modifiedProperties and flip the dirty flag.
  3. Then reset the value to either "" or 0 depending on if it's a number value or not.
  4. If it's not an internal model property, go ahead and delete it with the Reflect object.

The proxy also traps set and does the following:

  1. Determines if the property being changed is on the internal model object.
  2. If it is an internal model property, add to modifiedProperties and flip the dirty flag.
  3. Use Reflect to forward with changing the value.
const Modela = Object.create(null, {  
  init: {
    /*
      On initialization, an object argument should be passed in 
      representing the fields of resultant object

      Example:
      Modela.init({
          id: 0,
          firstName: '',
          lastName: ''
        });
    */
    value: function (model) {

      // Makes the target object dirty and stores modified properties since last save
      function modify (target, property) {
        if (property in model) {
          target.dirty = true;
          if (target.modifiedProperties.indexOf(property) == -1 
                && property !== "dirty" 
                && property !== "modifiedProperties") {
            target.modifiedProperties.push(property);
          }
        }
      }

      /*
        Define the base object with public methods
      */
      const base = Object.create(null, {
        // Array property to hold other modified properties
        modifiedProperties: {
          value: [],
          enumerable: true,
          writable: true
        },
        // Non-enumerable property to display the internal state
        __instance__: {
          value: model
        },
        save: {
          value: function () {
            // For each modified model property, save to internal state
            this.modifiedProperties.forEach(property => {
              model[property] = this[property];
            });

            // State saved, clear modified properties
            this.modifiedProperties = [];

            // Object now clean
            this.dirty = false;

            return this;
          },
          enumerable: true
        }
      });

      // Assign the model properties & value to the base object
      Object.assign(base, model);

      // Save the object to reset dirty flag
      base.save();

      /*
        Define the handler that traps required properties and implement
        custom logic for each one
      */
      const handler = {
        set (target, property, value) {
          modify(target, property);
          return Reflect.set(target, property, value);
        },
        deleteProperty (target, property) {
          modify(target, property);
          if (property in model) {
            if (Number.isNaN(model[property])) {
              target[property] = '';
            } else {
              target[property] = 0;
            }
            return true;
          }
          return Reflect.deleteProperty(target, property);
        }
      };

      // Construct the new proxy and return it
      const proxy = new Proxy(base, handler);
      return proxy;
    }
  }
});


var Product = Modela.init({  
  'id':0, 
  'title': '',
  'description': '',
  'price': 0,
  'quantity': 0
});

Proxy in Action

Let's see this in action. First, I modify one of the model properties, title. You'll see that it gets added to the modifiedProperties array, the dirty flag gets set to true, and the transient property value changes. However, the internal title property does not.

Then, when I invoke save(), the internal title property value gets updated.

Next, I add a non-model property to the object.

  • Note that the dirty flag does not get flipped.
  • Then I change the value of a internal model property, and both modifiedProperties and dirty are changed appropriately.
  • The internal property still hasn't changed value.
  • Then I invoke save() again and the internal property gets the value.

Lastly, I delete the price property, but because it's an internal model property, my trap stops the deletion from happening, and changes the value of the property back to 0.

Next Steps

From here, it's not a large jump in complexity to add in logic that persists the internal state to disk. You could write SQL with a database adapter, or work with an ORM tool to manage that process.

Also, I could have written better type checking to include booleans, nullable values, etc. but that's beyond the scope of this article.

Blog Logo

Steve Brownlee

Head Coach at Nashville Software School. Evolving software development education.


Published