How to create a Deep Proxy?

Fixed a bunch of bugs in my original question. I think this works now:

function createDeepProxy(target, handler) {
  const preproxy = new WeakMap();

  function makeHandler(path) {
    return {
      set(target, key, value, receiver) {
        if (typeof value === 'object') {
          value = proxify(value, [...path, key]);
        }
        target[key] = value;

        if (handler.set) {
          handler.set(target, [...path, key], value, receiver);
        }
        return true;
      },

      deleteProperty(target, key) {
        if (Reflect.has(target, key)) {
          unproxy(target, key);
          let deleted = Reflect.deleteProperty(target, key);
          if (deleted && handler.deleteProperty) {
            handler.deleteProperty(target, [...path, key]);
          }
          return deleted;
        }
        return false;
      }
    }
  }

  function unproxy(obj, key) {
    if (preproxy.has(obj[key])) {
      // console.log('unproxy',key);
      obj[key] = preproxy.get(obj[key]);
      preproxy.delete(obj[key]);
    }

    for (let k of Object.keys(obj[key])) {
      if (typeof obj[key][k] === 'object') {
        unproxy(obj[key], k);
      }
    }

  }

  function proxify(obj, path) {
    for (let key of Object.keys(obj)) {
      if (typeof obj[key] === 'object') {
        obj[key] = proxify(obj[key], [...path, key]);
      }
    }
    let p = new Proxy(obj, makeHandler(path));
    preproxy.set(p, obj);
    return p;
  }

  return proxify(target, []);
}

let obj = {
  foo: 'baz',
}


let proxied = createDeepProxy(obj, {
  set(target, path, value, receiver) {
    console.log('set', path.join('.'), '=', JSON.stringify(value));
  },

  deleteProperty(target, path) {
    console.log('delete', path.join('.'));
  }
});

proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'

You can assign full objects to properties and they'll get recursively proxified, and then when you delete them out of the proxied object they'll get deproxied so that you don't get notifications for objects that are no longer part of the object-graph.

I have no idea what'll happen if you create a circular linking. I don't recommend it.


Here's a simpler one that does what I think you wanted.

This example allows you to get or set any properties deeply, and calls a change handler on any property (deep or not) to show that it works:

function createOnChangeProxy(onChange, target) {
return new Proxy(target, {
    get(target, property) {
        const item = target[property]
        if (item && typeof item === 'object') return createOnChangeProxy(onChange, item)
        return item
    },
    set(target, property, newValue) {
        target[property] = newValue
        onChange()
        return true
    },
})
}

let changeCount = 0
const o = createOnChangeProxy(() => changeCount++, {})

o.foo = 1
o.bar = 2
o.baz = {}
o.baz.lorem = true
o.baz.yeee = {}
o.baz.yeee.wooo = 12

console.log(changeCount === 6)

const proxy = createOnChangeProxy(() => console.log('change'), {})
const baz = {baz: 9, quux: {duck: 6}};

proxy.foo = 5;
proxy.bar = baz;
proxy.bar.baz = 10;
proxy.bar.quux.duck = 999;

baz.quux.duck = 777;
delete proxy.bar;
delete proxy.bar; // should not trigger notifcation -- property was already deleted
baz.quux.duck = 666;  // should not trigger notification -- 'bar' was detached

console.log(proxy);

In the part that uses your code sample, there are no extra notifications like your comments wanted.


@mpen answer ist awesome. I moved his example into a DeepProxy class that can be extended easily.

class DeepProxy {
    constructor(target, handler) {
        this._preproxy = new WeakMap();
        this._handler = handler;
        return this.proxify(target, []);
    }

    makeHandler(path) {
        let dp = this;
        return {
            set(target, key, value, receiver) {
                if (typeof value === 'object') {
                    value = dp.proxify(value, [...path, key]);
                }
                target[key] = value;

                if (dp._handler.set) {
                    dp._handler.set(target, [...path, key], value, receiver);
                }
                return true;
            },

            deleteProperty(target, key) {
                if (Reflect.has(target, key)) {
                    dp.unproxy(target, key);
                    let deleted = Reflect.deleteProperty(target, key);
                    if (deleted && dp._handler.deleteProperty) {
                        dp._handler.deleteProperty(target, [...path, key]);
                    }
                    return deleted;
                }
                return false;
            }
        }
    }

    unproxy(obj, key) {
        if (this._preproxy.has(obj[key])) {
            // console.log('unproxy',key);
            obj[key] = this._preproxy.get(obj[key]);
            this._preproxy.delete(obj[key]);
        }

        for (let k of Object.keys(obj[key])) {
            if (typeof obj[key][k] === 'object') {
                this.unproxy(obj[key], k);
            }
        }

    }

    proxify(obj, path) {
        for (let key of Object.keys(obj)) {
            if (typeof obj[key] === 'object') {
                obj[key] = this.proxify(obj[key], [...path, key]);
            }
        }
        let p = new Proxy(obj, this.makeHandler(path));
        this._preproxy.set(p, obj);
        return p;
    }
}

// TEST DeepProxy


let obj = {
    foo: 'baz',
}


let proxied = new DeepProxy(obj, {
    set(target, path, value, receiver) {
        console.log('set', path.join('.'), '=', JSON.stringify(value));
    },

    deleteProperty(target, path) {
        console.log('delete', path.join('.'));
    }
});


proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'