How does `el.style.foo` in browsers have a value descriptor that acts as a getter/setter?

It seems as if a browser engine breaks the rules of the language. Can this be spec'd so that users can do things practically?

For example, right now, document.body.style.color is a {value: ...} descriptor, but overriding it with a getter breaks it and Chrome will no longer update the style when setting a value. This implies that Chrome has some sort of setter behavior attached to the value descriptor, and we've no way to call the original setter when patching the descriptor. If the browser uses something like Object.observe, then overriding the value descriptor to a getter/setter descriptor would still work.

Can the language be spec'd in some way that makes browsers be spec compliant, and allows users to patch properties and get reasonable results? For example, what if something like Object.observe made a comeback, and browser used this as a way to explain how they observe such properties?

I'm pretty sure this isn't in violation of the spec, it's just that it's an exotic object, much like a Proxy could be.

Like the humble array exotic object:

const a = [0];

Object.getOwnPropertyDescriptors(a);
//  {"0":{"value":0,"writable":true,"enumerable":true,"configurable":true},"length":{"value":1,"writable":true,"enumerable":false,"configurable":false}}

a.length = 0;

Object.getOwnPropertyDescriptors(a));

// {"length":{"value":0,"writable":true,"enumerable":false,"configurable":false}}

Do exotic objects have custom behavior added to them as some sort of plugin to the JS engine? Or does the JS engine spec a special internal API that can be used for observation?

What I'm wondering is, if something can be spec'd such that browsers can use it, but JS users can too, and it would explain the magic of what a browser (or JS engine, in cases like Array) is doing, and would also allow the user to achieve the same and to patch native objects in such a way that doesn't interfere with the mechanism that the native side (browser of JS) is using.

For example,

  • suppose that something like Object.observe is revived, and has a synchronous option for observing value properties on am arbitrary object
    • this could be used to explain how browsers or JS engines observe and react to value property changes.
  • alternatively, suppose the the hot topic of signals and effects gains us a Signals API in JS, and allows us something like signalify(obj, 'key1', 'key2') which would make obj.key1 and obj.key2 backed by signals, observable in effects, for sake of example.
    • this would allow browsers/engines to explain behavior because you would be able to think of array.length as the engine doing signalify(this, 'length') and observing value changes in a synchronous effect, while users would be able to also do similar (f.e. signalify style.transform, no-op if already signalified, or maybe it returns a signal that only the reference owner can use, etc)

In either of these two possibilities,

  • there would be a way to fully explain the native behavior,
  • users could do the same things with their objects,
  • and they could also observe the native properties without interrupting the native behavior.

Can something like this be achieved?

Proxy allows these objects to be created, the original owner is in control of the behavior.

Once that object is handed to something else the internal getters/setters can not be accessed. The only way to add setters (that still trigger the original) is to create a whole new object that "wraps" the original.

Conversation on Object.observe from the archived mailing list: An update on Object.observe

Yeah I know of Proxy. It has traps for everything though, including for example defineProperty. Does this mean native engines (JS or browser) are doing things wrong and not intercepting all the things they should be?

Nobody’s doing anything wrong - an exotic object can emulate all of these behaviors.

The spec defines how all JS objects must behave when interacted with through the meta-object-protocol (MOP) methods: ECMAScript® 2024 Language Specification

The way document.body.style.color is behaving does not break any of those rules/invariants.

Yeah, i get it. It's like if I were to give a user a Proxy to one of my objects, I don't have to guarantee that my Proxy's observation implementation continues to work after the define property trap is used. But the better Proxy would be when my Proxy observation implementation continues working after the user changes a descriptor.

Anywho, with that aside, I'm just curious if some sort of object observation API would

  1. be easier to use for behavior like array.length = 0 in the native side
  2. be easy for users to do the same thing with their objects, without having to use Proxy, thus without having to remember to implement every single trap properly which is quite difficult
  3. explain behavior we see today, while improving it such that a user can easily observe any objects that have native behavior like array.length it element.style.foo without the user breaking the native behavior

As an example, people are interested in signals right now. What if, for example, there were a way to get a read-only signal for the property of an object, like so:

function observeSomeArray(array) {
  // Get a read-only signal so we can observe changes to  array.length without breaking it
  const length = getSignal(array, 'length')

  // Now observe changes to the length
  createEffect(() => {
    // This effect re-runs any time length signal value changes when array.length changes
    console.log("Array length changed", length())
  })
}

observeSomeArray(array)

(The way signal+effect libs are implemented today, that effect and the signal will be garbage collected if nothing else references the array anymore, similar to event listeners on DOM elements)

If the array.length's value changes underneath the proxy-like, and doesn't actually change a regular property, that could be confusing if it doesn't trigger the signal/effect, but maybe signals should at least trigger upon get or set of a property because that's an opportunity to see it changed.

Also, this would be a chance to ensure that the spec requires such properties to always trigger a signal any time the value descriptor would change if the person were to observer it with a signal.

Nite, effects can be microtask-batched by default for performance, so that making 100 edits to the array length triggers a single effect in the next microtask (much like MutationObserver and how it allows us to coalesce updates in a single deferred callback specifically for performance).

My understanding is that one of the reasons Object.observe was dropped in favour of Proxy is that O.o made things more complex and had performance issues. Being able to retrospectively attach observers to an object after it was created meant that all objects needed to be able to handle that addition, whereas Proxy makes that decision at the time the object is created. Maybe someone on a JS engine team or who was around at the time could supply more history.

I don’t see a future for any sort of API like Object.observe, personally.