Javascript deep proxy to detect changes I'm trying to build a deep proxy to observe all the changes in an object in javascript. Here is what I got so far :

function GetDeepProxy(value, config, path = 'root')
{
    var handler = function(config) {
        return {
            set: function(target, name, value) {
                if(typeof config.beforeSet === 'function')
                {
                    config.beforeSet(target, name, value, this.path);
                }

                target[name] = GetDeepProxyHelper(value, handler(config), this.path + '.' + name);

                if(typeof config.afterSet === 'function')
                {
                    config.afterSet(target, name, value, this.path);
                }
                return true;
            }
        }
    };

    var GetDeepProxyHelper = function(value, handler, path)
    {
        handler.path = path;
        if(typeof value === 'object')
        {
            for(var prop in value)
            {
                if(typeof value[prop] === 'object')
                {
                    value[prop] = GetDeepProxyHelper(value[prop], handler, path + '.' + prop);
                }
            }
            return new Proxy(value, handler);
        }
        else
        {
            return value;
        }
    }

    return GetDeepProxyHelper(value, handler(config), path);
}


You can then use it like that :

var myConfig = {
    afterSet: function(target, name, value, path)
    {
        console.log('set :', path + '.' + name + ' = ', value);
    },
    beforeSet: function(target, name, value, path)
    {
        //console.log('setting :', path + '.' + name + ' = ', value);
    }
}

var proxy = GetDeepProxy({}, myConfig, 'proxy');

proxy.test = [];
proxy.test.push('hi');
proxy.test[0] = 'abc';
proxy.test.push('def');
proxy.test = proxy.test.slice(1);
proxy.deep = {};
proxy.deep.abc = 'abc';
proxy.deep2 = { abc: { test: {} } };
proxy.deep2.abc.test.value = 'test';

console.log(proxy);
console.log(JSON.stringify(proxy));


Which print the following result

(index):160 set : root.test =  []
(index):160 set : root.test.0 =  hi
(index):160 set : root.test.length =  1
(index):160 set : root.test.0 =  abc
(index):160 set : root.test.1 =  def
(index):160 set : root.test.length =  2
(index):160 set : root.test =  ["def"]
(index):160 set : root.deep =  {}
(index):160 set : root.deep.abc =  abc
(index):160 set : root.deep2 =  {abc: Proxy}
(index):160 set : root.deep2.abc.test.value =  test
(index):180 Proxy {test: Proxy, deep: Proxy, deep2: Proxy}
(index):181 {"test":["def"],"deep":{"abc":"abc"},"deep2":{"abc":{"test":{"value":"test"}}}}


Is there any modifications that I won't be able to observe using this approach and should I structure the code differently?
Missed 2, indirectly blind, know the rules, &amp; chasing the tail.

There are four problems with the functionality. The last one will present you with the biggest challenge, as proxy traps can be CPU costly and adding a check (which involves keeping a reference for each object as you step the path) may well be impractically slow.

The two you missed.

There are two additional method for modifying an object directly

An object can be modified via delete

proxy.test.a = "a";
delete proxy.test.a;  // this modification will be missed


An object can be modified via defineProperty

Object.defineProperty(proxy.test, 'c', {
    writable : false,
    enumerable : false,
    configurable : false,
    value : "definedProp",
});


You will need to use the deleteProperty and defineProperty traps to get them.

The one you don't see.

Also you will not see modification that are indirect.

const testObj = {a : 0, b : 1};
proxy.test[0] = testObj;  // this is trapped
proxy.test[0].c = "c";    // you trap this as well

testObj.c = 10;           // you miss this


The rules.

Though I am not sure if this was the intention but you are also picking up on inherited properties that may have been protected. You need to check that you can write to the property.

Object.defineProperty(Array.prototype, 'foo', {
    writable : false,
    enumerable : false,   // Also not enumerable! should you be looking?
    configurable : false,
    value : "bar",
})
proxy.test = [];
proxy.test.foo = "poo"; // You trap this as if the assignment is ok
// yet
console.log(proxy.test.foo); // outputs "bar"


Chasing the tail.

And the ever present problem that you face when doing deep searches is you have no protection against cyclic references.

It`s a little tricky as i was sure a simple cyclic reference would overflow the call stack, but that did not happen. With a little experimentation I did find a way to make it crash.

const myObj1 = {a : 0};
const myObj2 = {a : myObj1}
proxy.test = [];
proxy.test.push(myObj1)
proxy.test.push(myObj2)
proxy.test[0].a = myObj2;  // all good to this point

// this causes endless iteration and eventually the call stack will throw
// RangeError: Maximum call stack size exceeded
proxy.test[0].b = myObj1;