1

As demonstrated in this SO question, Proxy objects are a good way to watch for changes in an object.

What if you want to watch changes to subobjects? You'd need to proxy those subobjects as well.

I'm currently working with some code that does that automatically - any time you try to set a property it makes sure that it is wrapped in a proxy. We're doing this so that we can perform an action every time any value changes.

function createProxiedObject(objToProxy) {
  return new Proxy(objToProxy, {
    set: function (target, key, value) {
    //proxy nested objects
    if (value !== null && typeof value === 'object') {
      value = createProxiedObject(value);
    }
    target[key.toString()] = value;

    handleProxiedObjectChange();
  });

This works pretty well, but there is at least one case in which this backfires:

function ensureObjectHasProperty(object, key, default) {
  if (object[key] !== null) {
    // some validation happens here
    return object[key];
  } else {
    return default;
  }
}

...

proxiedObject = somethingThatCreatesAProxiedObject(someValue);
proxiedObject[someProperty] = ensureObjectHasProperty(proxiedObject, someProperty, defaultValue)

The result is that the value under someProperty (which is already being proxied) gets reassigned to the proxied object, causing it to get wrapped in another proxy. This results in the handleProxiedObjectChange method being called more than once each time any part of the object changes.

The simple way to fix it is to never assign anything to the proxied object unless it's new, but as this problem has already happened there's a reasonable chance someone will do it again in the future. How can I fix the set function to not rewrap objects that are already being proxied? Or is there a better way to watch an object so that handleProxiedObjectChange can be called any time the object or any of its subobjects change?

Community
  • 1
  • 1
Rob Watts
  • 6,866
  • 3
  • 39
  • 58
  • You'll probably have to keep track of already Proxied objects with something like a [`WeakMap`](https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), since, to my knowledge, you can't determine whether an object is an `instanceof Proxy`. – Decent Dabbler Oct 19 '16 at 23:25
  • @DecentDabbler why a `WeakMap`? – Rob Watts Oct 19 '16 at 23:37
  • In a WeakMap the keys are weak references to objects. This allows you to quickly search for an object, but won't prevent the garbage collector to remove the object from memory when there's no other references to the object anymore, thus preventing memory leaks. – Decent Dabbler Oct 19 '16 at 23:43
  • PS.: [See this answer](https://stackoverflow.com/a/36382536/165154) as well, as I believe the jsfiddle example in the answer has an implementation of this scenario, that may be of use to you. – Decent Dabbler Oct 19 '16 at 23:46
  • 1
    Sorry, a minor correction: a [WeakSet](https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) is probably more appropriate (which is also used in the aforementioned answer), since you probably don't have a need to map the objects to a particular value — you merely want to check whether an object is already in the Proxy list. Furthermore, the question that the aforementioned answer was addressing has [another (less complicated) answer](https://stackoverflow.com/a/37198132/165154) that is probably of interest to you as well. But you probably saw that already. :) – Decent Dabbler Oct 20 '16 at 00:09

1 Answers1

1

As suggested by @DecentDabbler, using a WeakSet allowed me to ensure I never try to wrap a proxy in another proxy:

const proxiedObjs = new WeakSet();

...

function createProxiedObject(objToProxy) {
  // Recursively ensure object is deeply proxied
  for (let x in objToProxy) {
    subObj = objToProxy[x];
    if (subObj !== null && typeof subObj === 'object' && !proxiedObjs.has(subObj)) {
      objToProxy[x] = createProxiedObject(subObj);
    }
  }

  let proxied = new Proxy(objToProxy, {
    set: function (target, key, value) {
      //This check is also new - if nothing actually changes
      //I'd rather not call handleProxiedObjectChange
      if (_.isEqual(target[key.toString()], value)) {
        return true;
      }

      //proxy nested objects
      if (value !== null && typeof value === 'object' && !proxiedObjs.has(value)) {
        value = createProxiedObject(value);
      }
      target[key.toString()] = value;

      handleProxiedObjectChange();
  });
  proxiedObjs.add(proxied);
  return proxied;
Rob Watts
  • 6,866
  • 3
  • 39
  • 58