57

Is there an alternative approach?

Is there another way to do change detection in object?

There is the Proxy method, but can anyone tell me how can I achieve this using Proxy:

var obj = {
  foo: 0,
  bar: 1
};

Object.observe(obj, function(changes) {
  console.log(changes);
});

obj.baz = 2;
// [{name: 'baz', object: <obj>, type: 'add'}]

obj.foo = 'hello';
// [{name: 'foo', object: <obj>, type: 'update', oldValue: 0}]
superluminary
  • 38,944
  • 21
  • 142
  • 143
Shad
  • 919
  • 1
  • 10
  • 18

3 Answers3

56

You can achieve this with getters and setters.

var obj = {
  get foo() {
    console.log({ name: 'foo', object: obj, type: 'get' });
    return obj._foo;
  },
  set bar(val) {
    console.log({ name: 'bar', object: obj, type: 'set', oldValue: obj._bar });
    return obj._bar = val;
  }
};

obj.bar = 2;
// {name: 'bar', object: <obj>, type: 'set', oldValue: undefined}

obj.foo;
// {name: 'foo', object: <obj>, type: 'get'}

Alternatively, in a browser with support for Proxies, you can write a more generic solution.

var obj = {
  foo: 1,
  bar: 2
};

var proxied = new Proxy(obj, {
  get: function(target, prop) {
    console.log({ type: 'get', target, prop });
    return Reflect.get(target, prop);
  },
  set: function(target, prop, value) {
    console.log({ type: 'set', target, prop, value });
    return Reflect.set(target, prop, value);
  }
});

proxied.bar = 2;
// {type: 'set', target: <obj>, prop: 'bar', value: 2}

proxied.foo;
// {type: 'get', target: <obj>, prop: 'bar'}
Juampi
  • 3,074
  • 1
  • 18
  • 20
Dan Prince
  • 27,111
  • 12
  • 81
  • 112
  • 1
    Is there an advantage to using the reflection API in this case (as opposed to simple object-access)? – Emissary Mar 28 '16 at 09:23
  • 4
    @Emissary using object access will trigger the proxy traps again causing the infinite loop. – Dan Prince Mar 28 '16 at 09:25
  • 2
    Thanks but I can't replicate an infinite loop with `target[prop] = value` - what environment are you running in? Also shouldn't the proxy replace the original variable in order to "observe" else `obj.bar = x` doesn't do what is suggested here. – Emissary Mar 28 '16 at 10:03
  • 2
    Might be something to do with incomplete proxy support. Much more in depth discussion on this [here](http://stackoverflow.com/questions/25421903/what-does-the-reflect-object-do-in-javascript). – Dan Prince Mar 28 '16 at 11:13
  • This is fine if you own the object and you pre-meditate having getters/setters or a Proxy. However, if you receive a reference to some other object, you could be damaging it by defining getters/setters on existing properties. `Object.observe` would not have had this problem if it accepted into the language. Proxy also have limitations that `Object.observe` does not, in the sense that you must pre-meditate making your objects observable, whereas `Object.observe` works on arbitrary 3rd-party references. – trusktr Jul 24 '18 at 17:53
  • 1
    @DanPrince `Reflect.get` does trigger get traps. [MDN](https://beta.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get#Interceptions). – Mason Aug 07 '19 at 22:37
  • 1
    @Mason your point is important to understand, so I'll put my 2 cents here: either object access or `Reflect.get` - both __will__ trigger the traps if running on __proxy__, __none__ of them will be trapped if running against the __target__ object. So the important point here is, that one should work with the `target` within the traps, less important which access method is used. – GullerYA Jan 25 '20 at 22:05
  • neither getters and setters or proxies work to debug angularjs propierties, they all break the code – Ivan Castellanos Feb 26 '20 at 19:54
15

Disclaimer: I'm the author of the object-observer library suggested below.

I'd not go with getters/setters solution - it's complicated, not scalable and not maintainable. Backbone did their two-way binding that way and the boilerplate to get it working correctly was quite a piece of a code.

Proxies is the best way to achieve what you need, just add to the examples above some callbacks registration and management and execute them upon a changes.

As regarding to the polyfill libraries: some/most of these implemented utilizing 'dirty check' or polling technique - not efficient, not performant. Occasionally, this is the case of the polyfill pointed out by Nirus above.

I'd recommend to pick up some library that does observation via Proxies. There are a few out there, object-observer being one of them: written for this use-case exactly, utilizes native Proxies, provides deep-tree observation etc.

GullerYA
  • 719
  • 6
  • 20
  • 1
    What about browser support? [**Proxies can't be polyfilled or transpiled**](https://stackoverflow.com/questions/35025204/javascript-proxy-support-in-babel), so IMO using Proxies isn't really a good idea in production environments just yet! – John Slegers Jul 14 '17 at 23:23
  • 2
    Right, this implementation won't run on any environment that is not supporting Proxy objects. Yet, all the downloadable major browsers are already there (Chrome, Firefox, Opera) and actually Edge as well. Mobile versions are also there. So the only concern I can see is IE pre-Edge - well, personally I've left it behind. – GullerYA Nov 26 '17 at 07:07
  • In the corporate world, dropping IE support often isn't an option. – John Slegers Nov 26 '17 at 13:19
  • 3
    :) being corporate employee myself - you're breaking into an open door! Well, this project is kind of a desire to express myself + my own helper for my own web apps + expression of extreme dislike for anything like Angular/React and others. I'm nowadays writing things with it integrating it heavily with CustomElements and enjoying the new world. No IE there, for good and for bad. – GullerYA Nov 26 '17 at 15:50
13

@Dan Prince solution should be the first choice always.

Just in case for some reason if you want to support browsers that are quite older, i would suggest you to go for any polyfill libraries available on Github or use Object.defineProperties API which is supported in IE 9 to emulate the same.

var obj = Object.defineProperties({}, {
    "foo":{
        get:function(){
            console.log("Get:"+this.value);
        },
        set:function(val){
            console.log("Set:"+val);
            this.value = val;
        }
    },

    "bar":{         
        get:function(){
            console.log("Get:"+this.value);
        },
        set:function(val){
            console.log("Set:"+val);
            this.value = val;
        }
    }
 });

Note: This is not a scalable solution. Make an educated decision whether to use the above API for larger data objects and computation intensive requirements.

Nirus
  • 3,078
  • 2
  • 18
  • 42