Skip to content

Instantly share code, notes, and snippets.

@ebidel
Last active July 29, 2021 04:08
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ebidel/1b553d571f924da2da06 to your computer and use it in GitHub Desktop.
Save ebidel/1b553d571f924da2da06 to your computer and use it in GitHub Desktop.
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
Copy link

eorroe commented Aug 14, 2015

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

@eorroe
Copy link

eorroe commented Aug 14, 2015

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

@ebidel
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link
Author

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
Copy link

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