How to watch variables in JavaScript


We all have to track variables while debugging; generally the easier it is to monitor changes, the faster bugs can be detected and fixed.

Web developer tools expose various methods for tracking changes in variable values. There are a couple of drawbacks e.g. non-uniform support across platforms) but again, half-bread is better than none :).

Curious about such methods? Let’s take a walk…

1. Chrome’s Object.Observe

First on the list is Chrome’s Object.observe static method. The observe/unobserve functions are static functions – they can only be called on Object and do not exist on object instances. Lets take an example:

var obj = {};
Object.observe(obj, function(changes) {
    console.log(changes[0]);
});

obj.val = "Hello"

// [{  name: "val",
//     object: { val: "Hello" },
//     type: "add" }]

That was pretty cool right? How about observing primitives?

var val = 2;

Object.observe(val, function(changes) {
    console.log(changes[0]);
});

//Console
//TypeError: Object.observe cannot
//observe non-object

Disappointed? Maybe we should check out Firefox’s watch function then…

TIP: Chrome also has experimental Array.observe and Array.unobserve methods. Don’t use in production!

2. Firefox’s Object.prototype.watch method

Firefox differs from Chrome because it exists on object instances; thus any object can watch for changes to its inner variables. Confused? Lets see an example:

window.val = 2;
//equal to var val = 2;

window.watch("val",function(id, old, cur) {
 console.log("Changed property: ", id);
 console.log("Original val: ", old);
 console.log("New val: ", cur);
});

val = 4;

// Changed property: val
// Original val: 2
// New val: 4

Advantages? Well, you can now watch primitives values. I also like the fact that object instances can watch their own properties for changes rather than having a global static watcher. But then again, it’s up to you :).

3. Can I haz polyfill?

A few browsers (including IE) do not have support for watch or observe; sometimes you do not have the free choice of selecting a browser, so let’s move on to finding an implementation that can be used as a polyfill and work across browsers.

First, lets talk about getters and setters in JS; JavaScript has the concept of getters and setters which allow you to define functions that are invoked when you try to retrieve a value or set its value.

var obj = {};

Object.defineProperty(obj, "name", {
    get : function(){ return this._name;},
    set: function(val){  this._name = val;}
});

obj.name;
//undefined

obj.name = "name";
obj.name;
//name

TIP: Can you figure out the bug in the following snippet?

var obj = {};

Object.defineProperty(obj, "name", {
    get : function(){ return this.name;},
    set: function(val){  this.name = val;}
});

obj.name;
//What happens?

Just as you thought, how about using this concept to fix this issue? Various browsers implement the getter/setter syntax differently, IE expects this format while Chrome expects this format. You could write a façade to hide this but again there is ugliness lurking around under the covers.

But great news! There is an excellent 5-year old polyfill by Eli Grey and it might meet your needs. Here is the gist and lets dive into its brilliance.

if (!Object.prototype.watch) {
 Object.defineProperty(
   Object.prototype,
   "watch", {
     enumerable: false,
     configurable: true,
     writable: false,
     value: function (prop, handler) {
       var old = this[prop];
       var cur = old;
       var getter = function () {
          return cur;
       };
       var setter = function (val) {
        old = cur;
        cur =
          handler.call(this,prop,old,val);
        return cur;
       };

       // can't watch constants
       if (delete this[prop]) {
        Object.defineProperty(this,prop,{
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
       }
    }
 });
}

How it works?

The function assumes knowledge of JavaScript’s accessor support (surprised that JS has getters and setters, yes there are subtle pockets of beauty in the language). The object.watch polyfill function accepts a property to watch and a handler to call when that function changes.

The handler function uses a beautiful trick – it redefines the watched value in the same scope, this ensures that the handler is always invoked when that value changes.

The trick to understanding the polyfill is in the second Object.defineProperty call. The value to be watched is first deleted to verify it is not a constant but this deletion will remove it from the scope. To counteract this, Eli’s brilliantly redefines the object in the same scope using Object.defineProperty. The getter is a plain getter while the setter is more involved – when the watched value is overridden or set to a new value, the setter is called. The closure in the watch object will invoke the handler after setting property to its new value. Neat huh?

Conclusion

Tip: A good way to debug is to use the debugger with an observed variable as shown below:

Object.observe("value",
   function() { debugger; });

This will cause the debugger to kick in whenever the value changes and this helps a lot in tracking down bugs.

1. Eli’s polyfill didn’t fire in a deterministic manner when I tried it on Chrome but it did work.

2. Remember there are going to be performance implications but again this assumes you are in debugging mode :)

And that’s it! Feel free to copy over Eli’s implementation and use for your code.

3 Comments

·

Leave a Reply

  1. Uncaught SyntaxError: Unexpected identifier

    is thrown for newval bare word on line 22 of the polyfill. newval is not mentioned any other time in this article. Am i missing something, or is it obviously broken with no explaination??

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.