Polyfilling Object.observe

# /#!/JoePea (6 years ago)

Is there a way to polyfill Object.observe in such a way that the object before observation is the same reference as the object being observed after the call (i.e. not a Proxy), and other than monkey-patching getters/setters?

Is defining getters/setters the only way?

# Ranando King (6 years ago)

The only way I can think of that might work will only work if all actions against the observed object happen through methods of the observed object's prototype. In that case, you can proxy the prototype. Short of that, you're out of luck.

# T.J. Crowder (6 years ago)

On Tue, Jul 24, 2018 at 6:01 PM, /#!/JoePea <joe at trusktr.io> wrote:

Is there a way to polyfill Object.observe in such a way that the object before observation is the same reference as the object being observed

after

the call (i.e. not a Proxy), and other than monkey-patching

getters/setters?

Is defining getters/setters the only way?

Even that doesn't really polyfill it, because Object.observe got notifications of changes when new properties were created as well.

But yes, if you know the names of the properties in advance (the "shape" of the object, I believe, is the current parlance?), and if you want notifications of changes just to those properties, I think monkeypatching will be your simplest and most successful approach.

If you need to catch additions as well, I don't think there's any solution other than diffing, which is quite yucky.

-- T.J. Crowder

# /#!/JoePea (6 years ago)

But yes, if you know the names of the properties in advance

I don't because I'm using element-behaviors (which I wrote). Behaviors can be arbitrarily added and removed from an element, behaviors can observe any arbitrary attributes on an element, but the difficulty is in behaviors observing arbitrary properties on an element regardless of if the props already exist. Because an element may have any number of unknown behaviors added to it in the future, there's no way to pre-meditate the set of props that will be observed.

The only way I can think of that might work will only work if all actions

against the observed object happen through methods of the observed object's prototype

If I drop support for IE, maybe Proxy will help me.

The thing is, what are the implications? It seems a bit tricky to introduce a Proxy somewhere in the middle of a class hierarchy.

  • How do I return a Proxied this from a class in the middle of a hierarchy without changing patterns? Seems like I could use a single base constructor then move all construction logic to a construct method called by the base constructor, to make things easier. I do a similar hack now anyways in order to make ES5-style classes work with native Custom Elements.
  • How do we proxify a class prototype when using ES6 classes? Do we just SomeClass.prototype = new Proxy(SomeClass.prototype, handler)? Any implications of that?
  • Seems like Object.observe would've just been the easiest way to achieve what I want, if that hadn't been dropped.
  • I currently use MutationObserver to observe HTML element attribute changes, which is the like the equivalent of Object.observe for DOM. But the downside of this is that it only covers attributes, not instance properties. Plus, attributes are always strings, incurring performance overhead. And all the new frameworks delegate to instance properties, bypassing attributes, therefore bypassing MutationObserver observations.

Seems like Object.observe would be the simple magic tool that I keep circling back to.

In my specific use case (the behaviors), I'd just like to observe arbitrary instance props so that I can get performance gains from not observing attribute changes in cases where a framework is using instance props instead of attributes. Again, attributes are arbitrarily observable, while props are not.

If I am okay to drop support for IE (I'm a little skeptical), then I'd like to consider how Proxy might help, but it just seems more complicated that it ought to be compared to Object.observe (which my behaviors could use to observe elements).

# Andrea Giammarchi (6 years ago)

Proxy is limited, because it won't intercept deleteProperty or others as part of a prototype, so you need to replace the object with a proxied object, which is not exactly the same as Object.observe.

# T.J. Crowder (6 years ago)

On Tue, Jul 24, 2018 at 6:50 PM, /#!/JoePea <trusktr at gmail.com> wrote:

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

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

When you diff is totally up to your use case. IIRC, AngularJS used to do it upon exit from event handlers (or possibly both entry and exit) or when the application code asked it to.

I think this thread is getting fairly off-topic for this list, though. Perhaps do up a full runnable MCVE and post a Stack Overflow question or similar.

Good luck with it!

-- T.J. Crowder

# /#!/JoePea (6 years ago)

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: 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 once.
  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 for
            // 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, {
            ...descriptor,

            get() {
                return getValue()
            },

            set(value) {
                setValue(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'
# /#!/JoePea (6 years ago)

Another caveat of my implementation, for example, is that it adds getters/setters for properties that previously didn't exist. This will break code that checks existence of props. etc. etc. That's not a problem with a theoretical Object.observe.

# Oriol _ (6 years ago)

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

The main problem with that code is that the prototype property of a class is read-only, that's why you see no output. Blame classes instead of proxies. And then inside the proxy traps you should use the target instead of the receiver if you want to avoid infinite recursion. I have never had issues with proxies.

So the following is what I'm using

Your function has various problems like assuming that you can define symbol properties in property descriptors and read them later, ignoring the receiver when calling getters and setters, assuming all objects are extensible and all properties are configurable, etc.

# Alex Vincent (6 years ago)

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: jsfiddle.net/trusktr/hwfontLc/17

Because you are doing it wrong.

Proxies can only observe on the object that they're created for. By setting Foo.prototype to a proxy, you can only observe operations on Foo.prototype, not instances of Foo.

What you really want is to have new Foo() return a Proxy... which is counter-intuitive, admittedly, because we're all taught that a constructor should never explicitly return a value.

This problem has been solved, a few times, with the introduction of membranes in JavaScript. In that environment, you would start with Foo and wrap it in a membrane proxy. Then, in invoking new Foo(), the "construct" trap of that proxy would return another roxy automatically, probably using the same proxy handler but a different proxy target.

If it's any consolation, proxies are in general very hard to work with. You're only scratching the surface here. I recently gave a talk at TC39 (the standards body for ECMAScript) on membranes. One key takeaway is that the overhead in dealing with membrane-oriented proxies really is better off left to a library built for that purpose.

Tom van Cutsem is working on an article summarizing the current state of membranes. I'm not sure if he has approved its general release yet, so stay tuned...

Alex Vincent Edmonds, WA (on vacation)

# #!/JoePea (6 years ago)

Because you are doing it wrong.

Yeah, because using Proxy isn't simple like using Object.observe. I'm just showing that this is all extremely easy to do with Object.observe.

What you really want is to have new Foo() return a Proxy... which is

counter-intuitive, admittedly, because we're all taught that a constructor should never explicitly return a value.

I mentioned earlier I don't want to modify my entire class hierarchy just to make part of it observable, and have to tell people who extend my classes to change their patterns (it's a breaking change).

Again, not a problem with Object.observe.

membranes

I just read the articles. That's much too complicated. Plus, it only works on objects that are designed to be proxies before they are passed to other code.

I want to do this:

import anyObject from 'any-package-on-npm'

Object.observe(anyObject, console.log)

// logs stuff every time any-package-on-npm modifies the object.

If I use proxy, this is what happens:

import anyObject from 'any-package-on-npm'

const membrane = new ObservableMembrane({
  valueMutated: console.log
})

membrane.getProxy(anyObject)

// nothing, silence, crickets

Object.observe just works, and is soooooooooooooooo simple. I won't shoot myself in the foot with it. I'd like to make a one-way data flow implementation with it, using 3rd party web components, and Object.observe would make this incredibly simple.