Polyfilling Object.observe

/#!/JoePea joe at trusktr.io
Fri Jul 27 17:53:25 UTC 2018

> I don't think there's any solution other than diffing

And how would you diff without polling (while supporting IE)?

`Proxy` is powerful, but it's not as good as `Object.observe` would've been
for some very simple tasks.

Every time I wish I could use `Proxy` in a simple way, there's always some
issue with it. For example: https://jsfiddle.net/trusktr/hwfontLc/17

Why do I have to sacrifice the convenience of writing ES6 classes just to
make that work? And plus that introduced an infinite recursion that I
overlooked because I didn't treat the get/set the same way as we should
treat getters/setters and store the value in a different place. It's just
more complicated than `Object.observe`.

If we want to use ES6 classes, we have to come up with some convoluted
pattern for returning a Proxied object from a constructor possibly deep in
a class hierarchy, so that all child classes can use the proxied `this`.

Using `Proxy` like this is simply not ideal compared to `Object.observe`.

> not exactly the same as Object.observe

Yep :)

> When you diff is totally up to your use case

I'd like performant change notifications without interfering with object
structure (f.e. modifying descriptors) or without interfering with the way
people write code. I want to have synchronous updates, because that gives
me the ability to opt-in to deferring updates. If the API is already
deferred (f.e. polling like in the official and deprecated Object.observed
polyfill), then there's not a way to opt-in to synchronous updates.

I simply would like to observe an object with a simple API like:

import someObject from 'any-npm-package-that-could-ever-exist'

const thePropsIWantToObserve = ['foo', 'bar', 'baz']

Object.observeProps( someObject, thePropsIWantToObserve, (name, oldValue,
newValue) => {
  console.log('property on someObject changed:', name, oldValue, newValue)

I'd be fine if it only gave me two args, `name` and `newValue`, and I could
optionally cache the oldValue myself if I really wanted to, which
automatically saves resources by making that opt-in. I'd also want it to be
at the very least triggering observations on a microtask. Synchronous would
be better, so I can opt-in to deferring myself. Maybe and option can be
passed in to make it synchronous.


I won't shoot myself in the foot with `Object.observe`. I know what I plan
to do with the gun, if it ever comes to exist. If one builds a tank (an
API) and places a user in it, that user can't shoot themselves in the foot,
can they? (I'm anti-war pro-peace and against violence, that's just an
analogy.) It's like a drill: sure, some people aren't very careful when
they use drills the wrong way and hurt themselves? What about the people
who know how to use the drills? Maybe we're not considering those people
when deciding that drills should be outlawed because one person hurt
themselves with one.

Let's let people who know what they're doing make good use of the tool. A
careless programmer will still shoot themselves in the foot even without
Object.observe. There's plenty of ways to do it as is.

If someone can currently implement `Object.observe` by using polling with
diffing, or by hacking getter/setter descriptors, why not just let them
have the legitimate native implementation? For people who are going to
shoot their foot off anyways, let's let them at least impale their foot
efficiently instead of using a spoon, while the professionals can benefit
from the tool.

We've got libs like Backbone.js that make us write things like
`someObject.set('foo', 123)` so that we can have the same thing as
`Object.observe` provides. Backbone was big. This shows that there's people
that know how to use the pattern correctly. This is another example of a
library author having to tell end users to write code differently in order
to achieve the same goal as we'd simply have with `Object.observe`:
ideally we'd only need to write `someObject.foo = 123` which saves both the
author of `someObject` and the consumer of `someObject` time.

It'd simply be so nice to have `Object.observe` (and preferably a simpler
version, like my following example).

So for my use case, I'll use the following small implementation. You may
notice it has many caveats that are otherwise non-existent with
`Object.observe` like,

1. It doesn't consider inherited getters/setters.
2. It doesn't consider that `isObserved` can be deleted if someone else
sets a new descriptor on top of the observed descriptor.
3. It may trigger unwanted extra side-effects by call getters more than
4. etc.

`Object.observe` simply has not problems (in theory, because the
implementation which is on the native side does not interfere with the
interface on the JavaScript side)!

So the following is what I'm using, which works in my specific use cases
where the above caveats are not a problem:

const isObserved = Symbol()

function observe(object, propertyNames, callback) {
    let map

    for (const propName of propertyNames) {
        const descriptor = Object.getOwnPropertyDescriptor(object,
propName) || {}

        if (descriptor[isObserved]) continue

        let getValue
        let setValue

        if (descriptor.get || descriptor.set) {
            // we will use the existing getter/setter assuming they don't do
            // anyting crazy that we might not expect. (See? Another reason
            // Object.observe)
            const oldGet = descriptor.get
            const oldSet = descriptor.set

            getValue = () => oldGet.call(object)
            setValue = value => oldSet.call(object, value)
        else {
            if (!map) map = new Map

            const initialValue = descriptor.value
            map.set(propName, initialValue)

            delete descriptor.value
            delete descriptor.writable

            getValue = () => map.get(propName)
            setValue = value => map.set(propName, value)

        Object.defineProperty(object, propName, {

            get() {
                return getValue()

            set(value) {
                callback(propName, getValue())

            [isObserved]: true,

And the usage looks like:

const o = {
    foo: 1,
    bar: 2,
    get baz() {
        console.log('original get baz')
        return this._baz
    set baz(v) {
        console.log('original set baz')
        this._baz = v

observe(o, ['foo', 'bar', 'baz'], (propName, newValue) => {
    console.log('changed value:', propName, newValue)

o.foo = 'foo'
o.bar = 'bar'
o.baz = 'baz'

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180727/7b5de04d/attachment.html>

More information about the es-discuss mailing list