1
\$\begingroup\$

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?

\$\endgroup\$
2
\$\begingroup\$

Missed 2, indirectly blind, know the rules, & 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;
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer. Is there any way to see indirect modifications? \$\endgroup\$ – Gudradain Oct 7 '17 at 13:40
  • \$\begingroup\$ @Gudradain To trap the indirect modifications you would have to replace the object's reference/s with a proxy. You could deep scan the global scope for those references, and change them, but references can be hidden inside functions and closures that can not be accessed from outside, This is part of the Javascript security model and there is no workaround. \$\endgroup\$ – Blindman67 Oct 7 '17 at 17:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.