Skip to content

Instantly share code, notes, and snippets.

@eligrey
Created April 30, 2010 01:38
Star You must be signed in to star a gist
Save eligrey/384583 to your computer and use it in GitHub Desktop.
object.watch polyfill in ES5
/*
* object.watch polyfill
*
* 2012-04-03
*
* By Eli Grey, http://eligrey.com
* Public Domain.
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
*/
// object.watch
if (!Object.prototype.watch) {
Object.defineProperty(Object.prototype, "watch", {
enumerable: false
, configurable: true
, writable: false
, value: function (prop, handler) {
var
oldval = this[prop]
, newval = oldval
, getter = function () {
return newval;
}
, setter = function (val) {
oldval = newval;
return newval = handler.call(this, prop, oldval, val);
}
;
if (delete this[prop]) { // can't watch constants
Object.defineProperty(this, prop, {
get: getter
, set: setter
, enumerable: true
, configurable: true
});
}
}
});
}
// object.unwatch
if (!Object.prototype.unwatch) {
Object.defineProperty(Object.prototype, "unwatch", {
enumerable: false
, configurable: true
, writable: false
, value: function (prop) {
var val = this[prop];
delete this[prop]; // remove accessors
this[prop] = val;
}
});
}
@cannapages
Copy link

Awesome thank you!

@qhedges
Copy link

qhedges commented May 15, 2013

What if the property already has a getter and setter. Doesn't this override them?

@juriseng
Copy link

juriseng commented Dec 4, 2013

Thanks so much, this snippet saved me! :)

@jslegers
Copy link

jslegers commented Dec 7, 2013

Seems like a pretty elegant and optimal solution...

Nice job !

@jslegers
Copy link

jslegers commented Dec 7, 2013

I tried the following example from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch :

var o = { p: 1 };

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

o.p = 2;
o.p = 3;
delete o.p;
o.p = 4;

o.unwatch('p');
o.p = 5;

The code breaks after the 'delete' statement. Firefox (using its native implementation) still triggers the 'watch' statement while Chrome (using the polyfill) doesn't. It's a minor issue, but I figured it's still worth mentioning...

When I remove the 'delete' statement, both both browsers are in synch.

@dtpz
Copy link

dtpz commented Dec 8, 2013

Hi!

Thanks, this is great - I created my own version after seeing yours here:
https://github.com/dtpz/DataWatch/blob/master/DataWatch.js

It takes what you did to the next level by allowing mutation of the set value (though this must be explicit), taking it off of the object prototype, and allowing multiple watchers and possibility to unwatch a specific function (the same way it's done with events).

Please tell me what you think - and thanks!

@Zeokat
Copy link

Zeokat commented Feb 24, 2014

Very usefull piece of code, you saved my day. Thanks.

@stefek99
Copy link

Someone up above mentioned that the setter function does not work properly:

a = {b:"c", d:"e"}
   Object {b: "c", d: "e"}

a.watch("b", function() { console.log("change, new a: ", a) } ) 
   undefined

a.b = "new value of B"
   change, new a:  Object {d: "e", b: undefined}
   "new value of B"

a
   Object {d: "e", b: undefined}

UPDATE: 16 Sep 2015 - so happy that I've found my comment - the thing does not work for me. It wasn't working a year ago, it isn't working now.

@luisnomad
Copy link

Awesome, thank you!

@adriengibrat
Copy link

@XoseLluis has proposed a great imporvement to this piece of code,
I've rewrote it, optimized for minification + included some tests :
https://gist.github.com/adriengibrat/b0ee333dc1b058a22b66

The only limitation is, as @jslegers mentioned, that deleting a property remove the watchpoint !

@koolb
Copy link

koolb commented Aug 25, 2014

Thanks for this. Here's the coffee version (https://gist.github.com/koolb/fb9c8238372590814e34):
if (not Object::watch?)
Object.defineProperty Object.prototype, "watch",
enumerable: false, configurable: true, writeable: false, value: (prop, handler) ->
oldval = @[prop]
newval = oldval
getter = () -> newval
setter = (val) -> oldval = newval; newval = handler.call(@, prop, oldval, val)
Object.defineProperty @, prop, get: getter, set: setter, enumerable: true, configurable: true if delete @[prop]

if (not Object::unwatch?)
Object.defineProperty Object.prototype, "unwatch",
enumerable: false, configurable: true, writeable: false, value: (prop) ->
prop = @[prop]
delete @[prop]
@[prop] = val

@franleplant
Copy link

This is really nice! (comma first styling makes me want to kill my self though)

@flackjap
Copy link

flackjap commented Nov 9, 2014

If anyone else comes by and needs a version of this that doesn't calls back when the newly set value of an object key is the same as previous, here's my fork:

https://gist.github.com/flackjap/f318e6a2b316e4d9fa44

@robianmcd
Copy link

I can't get this working with DOM Elements. e.g. https://gist.github.com/robianmcd/ee9a5f10dca11e357c9c

Am I doing something wrong?

@MikeGarde
Copy link

I wanted to add a few keywords for people searching for this type of solution, I am using this with Node.JS / Express.JS and it works perfectly!

Thanks

@webarthur
Copy link

@LinboLen
Copy link

simple delete prop make it looks like not so perfect, if somewhere have call Object.defineProperty(target, targetProto) before watch , so the setter and getter (if exist) can't work

take a look at my modification, sorry for that i have not test it yet
https://gist.github.com/xsilen/e301dd675d27f3fa036a

and also sorry for unwatch implement

@achillesrasquinha
Copy link

Excellent!

@crongro
Copy link

crongro commented Jul 9, 2016

Thanks for this one.

@rokobuljan
Copy link

rokobuljan commented Nov 10, 2016

Seems there's an issue undefined on the oldval

fiddle demo

var myObject = {test:"a"};

myObject .watch("test", function (id, oldval, newval) {
console.log(oldval+' '+ newval);
});

myObject .test = "b"; // "a b"
myObject .test = "c"; // "undefined c"

@Swivelgames
Copy link

@rokobuljan That's not an issue... You're not returning the desired new value in the watch.

var myObject = {test:"a"};

myObject .watch("test", function (id, oldval, newval) {
    console.log(oldval+' '+ newval);
    return newval;
});

myObject .test = "b"; // "a b"
myObject .test = "c"; // "b c"

@mmtftr
Copy link

mmtftr commented Jan 31, 2017

Hey man! I made an awesome popup blocker with your snippet. https://gist.github.com/MMDF/d793c6dced90a41495a1dd61d6bf2f5b
Take a look and add it to Tampermonkey to use it. Thanks for the public snippet!

@alierdogan7
Copy link

This was a day saver. Thanks for your nice snippet :)

@georgir
Copy link

georgir commented May 15, 2017

A more generic version is possible. Instead of getting directly this[prop] at the start, the watch function should try to getOwnPropertyDescriptor on the object or along its prototype chain until it succeeds, then handle accessor properties by invoking the original getter/setter. This way you can watch html input elements "value" and "checked" properties for programmatic modification for example.

@mategvo
Copy link

mategvo commented Jul 28, 2017

Has anyone developed a version that would track children properties too?

@Erutan409
Copy link

@mateuszgwozdz Vue.js can do it.

@Erutan409
Copy link

@georgir What are you referring to, exactly? Can you provide an example?

@chack1172
Copy link

Is it possible to watch window and check when variable is set with var el?

@BSBProphet
Copy link

value: function( prop, handler ){
var oldval = this[prop],
getter = () => ( oldval ),
setter = function( val ){
if ( oldval != val ){ // event with a different value
handler.call( this, prop, oldval, val);
return ( oldval = val );
}
};

@rubenreyes2000
Copy link

rubenreyes2000 commented Apr 18, 2020

@stefek99 : the handler function must return the new value

alternatively, if you want a behavior more akin watchers in vue/react/angular you can modify the setter function as follows:

setter = function (val) {
	oldval = newval;
	return newval = (function() {
		handler.call(this, prop, oldval, val);
		return val;
	})();
};

if forces the new value to always be passed and assigned to the property, and the handler function becomes just "notified" of the change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment