Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Object.observe() polyfill using ES6 Proxies (POC)
// An `Object.observe()` "polyfill" using ES6 Proxies.
//
// Current `Object.observe()` polyfills [1] rely on polling
// to watch for property changes. Proxies can do one better by
// observing property changes to an object without the need for
// polling.
//
// Known limitations of this technique:
// 1. the call signature of `Object.observe()` will return the proxy
// object. The original object needs to be overwritten with this return value.
// See usage below.
// 2. Changes that happen quickly should be batched into a single
// callback. Current this is not the case. The callback gets called
// upon every change.
//
// [1]: https://github.com/jdarling/Object.observe/blob/master/Object.observe.poly.js
(function() {
'use strict';
// TODO: support 3rd param acceptList
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
var observe = function(obj, callback) {
if (Object(obj) !== obj) {
throw new TypeError('target must be an Object, given ' + obj);
}
if (typeof callback !== 'function') {
throw 'observer must be a function, given ' + callback;
}
return new Proxy(obj, {
set(target, propKey, value, receiver) {
var oldVal = target[propKey];
// Don't send change record if value didn't change.
if (oldVal === value) {
return;
}
let type = oldVal === undefined ? 'add' : 'update';
var changeRecord = {
name: propKey,
type: type,
object: target
};
if (type === 'update') {
changeRecord.oldValue = oldVal;
}
target[propKey] = value; // set prop value on target.
// TODO: handle multiple changes in a single callback.
callback([changeRecord]);
},
deleteProperty(target, propKey, receiver) {
// Don't send change record if prop doesn't exist.
if (!(propKey in target)) {
return;
}
var changeRecord = {
name: propKey,
type: 'delete',
object: target,
oldValue: target[propKey]
};
delete target[propKey]; // remove prop from target.
// TODO: handle multiple changes in a single callback.
callback([changeRecord]);
}
});
};
if (!Object.observe) {
Object.observe = observe;
}
})();
// ====== Tests ====== //
let x = {a: 5};
// If we were observing an object within a (e.g. x.a), that would
// need to also be the return variable and the argument to O.o().
// Note: using the native O.o(), you do not need to overwrite
// the original object with the return value.
x = Object.observe(x, function(changes) {
changes.forEach(function(c, i) {
console.log(c);
});
});
x.a = 10; // update
x.a = 10; // asserts no change record
x.b = 100; // add
delete x.b; // delete
delete x.b; // asserts no change record
@eorroe

This comment has been minimized.

Show comment Hide comment
@eorroe

eorroe Aug 14, 2015

Object.observe is not a method for each object, (shouldn't be on Object.prototype) just Object

eorroe commented Aug 14, 2015

Object.observe is not a method for each object, (shouldn't be on Object.prototype) just Object

@eorroe

This comment has been minimized.

Show comment Hide comment
@eorroe

eorroe Aug 14, 2015

Also the native Object.observe does return, so returning the Proxied object is good.

eorroe commented Aug 14, 2015

Also the native Object.observe does return, so returning the Proxied object is good.

@ebidel

This comment has been minimized.

Show comment Hide comment
@ebidel

ebidel Aug 14, 2015

@eorroe right on Object.prototype. I updated the gist to match that. The native does return, but for this technique to work, you need overwrite the original object with the proxied return value. At least that's the only way I could get things to cook.

Owner

ebidel commented Aug 14, 2015

@eorroe right on Object.prototype. I updated the gist to match that. The native does return, but for this technique to work, you need overwrite the original object with the proxied return value. At least that's the only way I could get things to cook.

@addyosmani

This comment has been minimized.

Show comment Hide comment
@addyosmani

addyosmani Aug 14, 2015

Nice! As noted, proxies are ~copies of the original objects and changes need to be made to the proxied object rather than the original one. As such, you'll get close to the original behaviour but not 100% there (proxy traps would run after the change vs batching from O.o). Your implementation might also want to consider adding in the inverse (unobserve()) API method, getNotifier for creating user defined notifications (which should work with proxies), deliverChangeRecords (deliver notifications collected for the handler sync).

@MaxArt2501 wrote a well done Object.observe polyfill over at https://github.com/MaxArt2501/object-observe which IIRC involved some similar research around proxies for O.o: https://github.com/MaxArt2501/object-observe/blob/master/doc/index.md

Nice! As noted, proxies are ~copies of the original objects and changes need to be made to the proxied object rather than the original one. As such, you'll get close to the original behaviour but not 100% there (proxy traps would run after the change vs batching from O.o). Your implementation might also want to consider adding in the inverse (unobserve()) API method, getNotifier for creating user defined notifications (which should work with proxies), deliverChangeRecords (deliver notifications collected for the handler sync).

@MaxArt2501 wrote a well done Object.observe polyfill over at https://github.com/MaxArt2501/object-observe which IIRC involved some similar research around proxies for O.o: https://github.com/MaxArt2501/object-observe/blob/master/doc/index.md

@ebidel

This comment has been minimized.

Show comment Hide comment
@ebidel

ebidel Aug 14, 2015

proxies are ~copies of the original objects and changes need to be made to the proxied object rather than the original one

This is why ones needs to overwrite the original object with the proxied version:

let x = {a: 5};
x = Object.observe(x, function(changes) {
  changes.forEach(function(c, i) {
    console.log(c);
  });
});```

Hacky, but it works.
Owner

ebidel commented Aug 14, 2015

proxies are ~copies of the original objects and changes need to be made to the proxied object rather than the original one

This is why ones needs to overwrite the original object with the proxied version:

let x = {a: 5};
x = Object.observe(x, function(changes) {
  changes.forEach(function(c, i) {
    console.log(c);
  });
});```

Hacky, but it works.
@ebidel

This comment has been minimized.

Show comment Hide comment
@ebidel

ebidel Aug 14, 2015

Also turns out @arv did a fairly spec complete version in 2012: https://mail.mozilla.org/pipermail/es-discuss/2012-July/024111.html

Owner

ebidel commented Aug 14, 2015

Also turns out @arv did a fairly spec complete version in 2012: https://mail.mozilla.org/pipermail/es-discuss/2012-July/024111.html

@samthor

This comment has been minimized.

Show comment Hide comment
@samthor

samthor Feb 29, 2016

Hey folks, we've just published a Proxy polyfill, which could work in older browsers to support this style of Object.observe polyfill: except that deleteProperty isn't supported, at least for now.

https://github.com/GoogleChrome/proxy-polyfill

samthor commented Feb 29, 2016

Hey folks, we've just published a Proxy polyfill, which could work in older browsers to support this style of Object.observe polyfill: except that deleteProperty isn't supported, at least for now.

https://github.com/GoogleChrome/proxy-polyfill

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