41

Going through Javascript documentation, I found the following two functions on a Javascript object looks interesting:

.watch - Watches for a property to be assigned a value and runs a function when that occurs.
.unwatch - Removes a watchpoint set with the watch method.


UPDATE: Deprecation warning
Do not use watch() and unwatch()! These two methods were implemented only in Firefox prior to version 58, they're deprecated and removed in Firefox 58+


Sample usage:

o = { p: 1 };
o.watch("p", function (id,oldval,newval) {
    console.log("o." + id + " changed from " + oldval + " to " + newval)
    return newval;
});

Whenever we change the property value of "p", this function gets triggered.

o.p = 2;   //logs: "o.p changed from 1 to 2"

I am working on Javascript for the past few years and never used these functions.
Can someone please throw some good use cases where these functions will come in handy?

vsync
  • 118,978
  • 58
  • 307
  • 400
Naga Kiran
  • 8,585
  • 5
  • 43
  • 53
  • 2
    These are available only for Gecko based browsers, like Mozilla Firefox. Internet Explorer exposes though a similar method on objects, called onpropertychange. – Ionuț G. Stan Jun 30 '09 at 14:12

8 Answers8

52

It's now 2018 and the answers to this question are a bit outdated:

  • Object.watch and Object.observe are both deprecated and should not be used.
  • onPropertyChange is a DOM element event handler that only works in some versions of IE.
  • Object.defineProperty allows you to make an object property immutable, which would allow you to detect attempted changes, but it would also block any changes.
  • Defining setters and getters works, but it requires a lot of setup code and it does not work well when you need to delete or create new properties.

Today, you can now use the Proxy object to monitor (and intercept) changes made to an object. It is purpose built for what the OP is trying to do. Here's a basic example:

var targetObj = {};
var targetProxy = new Proxy(targetObj, {
  set: function (target, key, value) {
      console.log(`${key} set to ${value}`);
      target[key] = value;
      return true;
  }
});

targetProxy.hello_world = "test"; // console: 'hello_world set to test'

The only drawbacks of the Proxy object are:

  1. The Proxy object is not available in older browsers (such as IE11) and the polyfill cannot fully replicate Proxy functionality.
  2. Proxy objects do not always behave as expected with special objects (e.g., Date) -- the Proxy object is best paired with plain Objects or Arrays.

If you need to observe changes made to a nested object, then you need to use a specialized library such as Observable Slim (which I authored). It works like this:

var test = {testing:{}};
var p = ObservableSlim.create(test, true, function(changes) {
    console.log(JSON.stringify(changes));
});

p.testing.blah = 42; // console:  [{"type":"add","target":{"blah":42},"property":"blah","newValue":42,"currentPath":"testing.blah",jsonPointer:"/testing/blah","proxy":{"blah":42}}]
Elliot B.
  • 17,060
  • 10
  • 80
  • 101
  • 7
    Not sure I understand your Proxy example. You write that it can intercept changes to the target object, but in your example you modify the property value through the proxy and not the target object. It is not clear how you can intercept changes to the target object with this. – Johncl Nov 24 '19 at 10:36
  • 2
    @Johncl That's just how the Proxy object works: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy whether the behavior is best described by the word "intercept", "virtualizes", "traps" or something else is somewhat up to interpretation. – Elliot B. Nov 24 '19 at 18:03
  • 1
    But this is totally different to listening to property changes that the original poster asked for. If you have an object with some properties and you pass that object into another black box - but the black box wants to listen to a change to a property in this object and act on that, the Proxy object above will not help at all. – Johncl Jan 10 '20 at 11:31
  • 2
    @Johncl It's not totally different -- with a `Proxy` you can achieve exactly the same end result. But yes, you are correct, you are not observing changes made *directly* to the target object -- that's implied by the name `Proxy`. – Elliot B. Jan 10 '20 at 18:31
  • 1
    @ElliotB. how can you listen to window.test obj ? For example when somebody changes window.test then console log it – strix25 Oct 15 '20 at 08:37
  • what do you mean *"it's now 2018"*? it's now 2020 ;) – vsync Oct 24 '20 at 09:31
  • @strix25 it is not possible to listen to arbitrary property changes on arbitrary objects and it shouldnt be possible ever, because it completely breaks nearly all optimizations. Contemporary javascript engines can reduce objects to as little as a C struct in some cases, and even in the less ideal case they usually become hashmaps. If they had to preserve them in a form where it's possible to convert them into key-independent accessor functions that would be a massive performance hit because every assignment is now possibly a call to an arbitrary JS function. – Lőrinc Bethlenfalvy Mar 27 '21 at 17:15
  • Actually, Object.defineProperty does allow mutability. You can use the 'get' and 'set' properties within it to override the property's getter/setter. This allows you to delegate the getter/setter to your own functions. – Maow Sep 20 '21 at 18:27
11

What watch is really designed for is validation of property values. For example you could validate that something is an integer:

obj.watch('count', function(id, oldval, newval) {
    var val = parseInt(newval, 10);
    if(isNaN(val)) return oldval;
    return val;
});

You could use it to validate string length:

obj.watch('name', function(id, oldval, newval) {
    return newval.substr(0, 20);
});

However, these are only available in the latest versions of the SpiderMonkey javascript engine. Great if you are using Jaxer or embedding the SpiderMonkey engine, but not really available in your browser yet (unless you are using FF3).

Prestaul
  • 83,552
  • 10
  • 84
  • 84
7

Check out Object.defineProperty and Object.prototype.\__defineGetter__ (or \__defineSetter__ ) to see where this functionality is heading.

Object.defineProperty should be available in all contemporary browsers real soon now.

Community
  • 1
  • 1
Sean Hogan
  • 2,902
  • 24
  • 22
2

You could take a look at the Javascript Propery Events library. It's a small library extending Object.defineProperty with some event callers, that I made recently. It adds a few on[event] properties that can be used like the on[event] properties of HTML-Objects. It also has a simple type check, which calls the onerror event if it fails.

Taking your code it would result in something like this:

var o = {}
Object.defineProperty(o, "p", {
    value:1,
    writable:true,
    onchange:function(e){
        console.log("o." + e.target + " changed from " + e.previousValue + " to " + e.returnValue);
    }
})
Jonatas Walker
  • 13,583
  • 5
  • 53
  • 82
2

Object.defineProperty

Promise

remove Promise and keep callback only if Promise is not supported in your target browser

Important:

1) Be aware of async behaviour on using promise.

2) Object.defineProperty doesn't trigger the callback, only assign operator '=' does

Object.onPropertySet = function onPropertySet(obj, prop, ...callback_or_once){
    let callback, once;
    for(let arg of callback_or_once){
        switch(typeof arg){
        case "function": callback = arg; break;
        case "boolean": once = arg; break;
        }
    }


    let inner_value = obj[prop];
    let p = new Promise(resolve => Object.defineProperty(obj, prop, {
        configurable: true,
        // enumerable: true,
        get(){ return inner_value; },
        set(v){
            inner_value = v;
            if(once){
                Object.defineProperty(obj, prop, {
                    configurable: true,
                    // enumerable: true,
                    value: v,
                    writable: true,
                });
            }
            (callback || resolve)(v);
        }
    }));
    if(!callback) return p;
};

// usage
let a = {};
function sayHiValue(v){ console.log(`Hi "${v}"`); return v; }

// do
Object.onPropertySet(a, "b", sayHiValue);
a.b = 2; // Hi "2"
a.b = 5; // Hi "5"

// or
Object.onPropertySet(a, "c", true).then(sayHiValue).then(v => {
    console.log(a.c); // 4 // because a.c is set immediatly after a.c = 3
    console.log(v); // 3 // very important: v != a.c if a.c is reassigned immediatly
    a.c = 2; // property "c" of object "a" is re-assignable by '=' operator
    console.log(a.c === 2); // true
});
a.c = 3; // Hi "3"
a.c = 4; // (Nothing)
Valen
  • 1,693
  • 1
  • 20
  • 17
1

Here's a simple alternative to watch/unwatch for an object literal using just a getter/setter. Whenever the p property is changed, any function can be called.

var o = {
 _p: 0,
  get p() {
    return this._p;
  },
  set p(p){    
    console.log(`Changing p from ${this._p} to ${p}`);
    this._p = p;    
    return this._p;
  }
}

o.p = 4;
o.p = 5;
Victor Stoddard
  • 3,582
  • 2
  • 27
  • 27
  • Will **not catch changes** for *array* or *objects*. Ex. - `o.p = [1,2,3]` and then `o.p.length = 1` – vsync Oct 24 '20 at 09:45
  • This answer is for object literals only, as shown in the question. I don't recommend its use for anything other than an object literal. – Victor Stoddard Oct 24 '20 at 21:20
0

Instead of using Proxy and therefore not "listening" directly on the original object you can use something like this:

const obj = {};

obj.state = {
  isLoaded: false,
  isOpen: false,
};

obj.setState = (newState) => {
  // Before using obj.setState => result Object { isLoaded: false, isOpen: false }
  console.log(obj.state); 
  obj.state = { ...obj.state, ...newState };
  // After using obj.setState ex. obj.setState({new:''}) => result Object { isLoaded: false, isOpen: false, new: "" }
  console.log(obj.state); 
};

That approach is more or less how it would work in ReactJS, you have source object in this example obj.state and setter obj.setState

You don't have to put both in one object like I did but seems like a good way to organize things

-1

You can use setInterval

Object.prototype.startWatch = function (onWatch) {

    var self = this;

    if (!self.watchTask) {
        self.oldValues = [];

        for (var propName in self) {
            self.oldValues[propName] = self[propName];
        }


        self.watchTask = setInterval(function () {
            for (var propName in self) {
                var propValue = self[propName];
                if (typeof (propValue) != 'function') {


                    var oldValue = self.oldValues[propName];

                    if (propValue != oldValue) {
                        self.oldValues[propName] = propValue;

                        onWatch({ obj: self, propName: propName, oldValue: oldValue, newValue: propValue });

                    }

                }
            }
        }, 1);
    }



}

var o = { a: 1, b: 2 };

o.startWatch(function (e) {
    console.log("property changed: " + e.propName);
    console.log("old value: " + e.oldValue);
    console.log("new value: " + e.newValue);
});
Taner
  • 149
  • 1
  • 3