Analog to Object.getPropertyDescriptor() for *changing* a property value?

# Axel Rauschmayer (14 years ago)

As a loose analog to the prototype-chain-traversing getPropertyDescriptor(), I would still like to have something that allows one to easily change properties higher up the prototype chain (e.g. to use a prototype to share state).

Maybe it would be enough to just have Object.getDefiningObject(obj, propName): www.mail-archive.com/[email protected]/msg06652.html

But I can also imagine syntactic sugar: obj.foo := "abc" desugars to Object.getDefiningObject(obj, "foo").foo = "abc"

# Brendan Eich (14 years ago)

On Jun 21, 2011, at 8:24 AM, Axel Rauschmayer wrote:

As a loose analog to the prototype-chain-traversing getPropertyDescriptor(), I would still like to have something that allows one to easily change properties higher up the prototype chain (e.g. to use a prototype to share state).

Such mutation from a delegating object, because it affects all other objects that delegate to the shared prototype, is usually a bug! Shared mutables are a bitch, even ignoring threads.

I think it would be better to require the mutation to have a direct reference to the prototype. Yes, it could be discovered via Object.getPrototypeOf -- assuming no proxies.

We cannot assume no proxies, and a Proxy is responsible for delegating to possibly hidden objects other than the |proto| passed to Proxy.create.

Even without proxies, or assuming they behave like native objects, mutation from one of N delegating objects is an anti-pattern.

# Axel Rauschmayer (14 years ago)

That sounds like the opposite argument you are making with regard to the hypothetical |here|:

BTW I do not agree we can or should try to reserve 'here' or expose the method's "home object" -- that breaks abstractions built using prototypes. 'super' does not, because it always goes to the [[Prototype]] of the object in which the method was defined.

The prototype seems to be the best location for shared data (such as the number of instances of a class). Wouldn’t you also want to abstract from the exact location of such a property?

I find that Common Lisp does this well, via "places":

  • Property lookup => returns a place, e.g. a pair (object, property name)
  • Use the place to either read the property value or to change it.

Can’t comment on Proxies, though.

# Brendan Eich (14 years ago)

On Jun 21, 2011, at 1:50 PM, Axel Rauschmayer wrote:

That sounds like the opposite argument you are making with regard to the hypothetical |here|:

BTW I do not agree we can or should try to reserve 'here' or expose the method's "home object" -- that breaks abstractions built using prototypes. 'super' does not, because it always goes to the [[Prototype]] of the object in which the method was defined.

No, the arguments are close cousins, in no way opposed.

You should not in general mutate shared prototype state via one of N delegating objects. It leads to trouble.

You should not depend on which of M prototype objects along the chain holds a method. Similar trouble with scaling M, evolving the program to increase M by interposing a new prototype which might inject a shadowing method for good reason, and so on.

The prototype seems to be the best location for shared data (such as the number of instances of a class).

Counting instances can't be done without leaking GC non-determinsm, so this is a bogus example. Can you come up with another? Shared mutable state in prototypes that is visibly mutable by means of a reference to one of N delegating objects is usually a bug.

# Axel Rauschmayer (14 years ago)

You should not in general mutate shared prototype state via one of N delegating objects. It leads to trouble.

What’s the difference to a global variable? I always thought about an instance in JavaScript as being composed of a non-shared object and a shared meta-object. Why should the former be able to change its state, but not the latter? Is there a fundamental difference between object and meta-object in this case?

The prototype seems to be the best location for shared data (such as the number of instances of a class).

Counting instances can't be done without leaking GC non-determinsm, so this is a bogus example. Can you come up with another? Shared mutable state in prototypes that is visibly mutable by means of a reference to one of N delegating objects is usually a bug.

One thing I did in pre-enum days in Java was to define constants and collect all values at the same time:

const red = new Color("FF0000"); const green = new Color("00FF00"); Color.prototype.getAllColors().forEach(...);

But you could also produce the colors with a factory.

# Brendan Eich (14 years ago)

On Jun 22, 2011, at 3:55 PM, Axel Rauschmayer wrote:

You should not in general mutate shared prototype state via one of N delegating objects. It leads to trouble.

What’s the difference to a global variable?

The list is long.

How about this? You have a bunch of local variables in various functions referencing different objects. But by some obscure yet legal means you use one such reference to mutate the prototype object they share in common, so all the others appear to be mutated too.

This is not inherently "evil", but it's usually a bug. Usually the other different objects do not wish to be mutated so indirectly, and the users of their properties (own and delegated) in those various functions may not be prepared for such a change.

Consider monkey-patching. You want to add value to a shared prototype, but you might break for-in loops. Ok, that can be solved with ES5 Object.defineProperty -- but not if the mutation breaks some other invariant.

It's occasionally useful to do AOP on shared prototypes. My point is that you ought to address them directly by their true names (Array.prototype, e.g.; not Object.getPrototypeOf(someArray)).

# Allen Wirfs-Brock (14 years ago)

On Jun 21, 2011, at 9:50 PM, Axel Rauschmayer wrote:

That sounds like the opposite argument you are making with regard to the hypothetical |here|:

BTW I do not agree we can or should try to reserve 'here' or expose the method's "home object" -- that breaks abstractions built using prototypes. 'super' does not, because it always goes to the [[Prototype]] of the object in which the method was defined.

The prototype seems to be the best location for shared data (such as the number of instances of a class). Wouldn’t you also want to abstract from the exact location of such a property?

Behaviorally, you shouldn't care how an object is implemented. Whether a particular property is implemented as an own property or is inherited from a prototype is an implementation detail that should be irrelevant to client code that is using an object. Such implementation details should be, at least conceptually, encapsulated by the object.

If you are dynamically inspecting or modifying details of the implementation of an object you have moved into the realm of reflection. Object.getPrototypeOf, Object.getOwnPropertyDescriptor, etc. are reflection operations. Generally it is poor practice to design an object such that client code needs to enter the domain of reflection to interact with the object. If client code uses these operations it is breaking implementation encapsulation.

One of the reason that the reflection functions added by ES5 are defined on Object rather than Object.prototype is to avoid polluting the public interface of every object with additional reflective operations over and beyond the legacy ones already defined in ES1-3. Arguably, even that separation isn't enough as Object really is in the application layer of the system rather than the reflection layer. In the long run it would be good if we could further separate the reflection layer by migrating to a Mirrors-based reflection model. See my series of blogs post about Mirrors for JavaScript www.wirfs-brock.com/allen/posts/228 for more details.

We probably still need to add some reflection methods to Object. defineMethod is one strong candidate. However, we really should be quite careful about what goes there as it is just too easy to use such Object.* functions to violate an object's implementation encapsulation.

# David Bruant (14 years ago)

In a the provided example, you assign a value. Can't you just use a getter/setter pair on the prototype chain? It sounds like it would solve your use case (without the syntax sugar). On ES5 - 8.12.5 step 4 & 5, the prototype chain is climbed to look up for an accessor and if there is a setter (anywhere on the prototype chain), this setter is called. Combined with the fact that a getter naturally do the same thing for getters, you have your way to change a value "directly on the prototype chain". Of course, if you want to change the property descriptor on the prototype chain, none of what I said apply.

David

Le 21/06/2011 17:24, Axel Rauschmayer a écrit :

# Axel Rauschmayer (14 years ago)

I guess my argument goes like this:

  • If the prototype of an instance loosely corresponds to a class, why shouldn’t we have class variables/class state?
  • If the location for reading data is abstracted over, why isn’t the location for writing data?
# Axel Rauschmayer (14 years ago)

How about this? You have a bunch of local variables in various functions referencing different objects. But by some obscure yet legal means you use one such reference to mutate the prototype object they share in common, so all the others appear to be mutated too.

This is not inherently "evil", but it's usually a bug. Usually the other different objects do not wish to be mutated so indirectly, and the users of their properties (own and delegated) in those various functions may not be prepared for such a change.

So you are mainly worried about mutating methods? My main concern is encapsulation: If you have class methods, I would want to give them the option of having class state. I want the same rules that hold for the object level (state + behavior) to hold for the meta-object level.

Consider monkey-patching. You want to add value to a shared prototype, but you might break for-in loops. Ok, that can be solved with ES5 Object.defineProperty -- but not if the mutation breaks some other invariant.

I would probably never use for-in loops in ES.next.

It's occasionally useful to do AOP on shared prototypes. My point is that you ought to address them directly by their true names (Array.prototype, e.g.; not Object.getPrototypeOf(someArray)).

Right, the latter looks really bad. I would rely that the same kind of unique name that took me to a class method (via "this") will also take me to a class variable (also via "this"). If I invoke the method via "this", e.g. Array.prototype.myClassMethod(), then this.myClassVariable would still work.

# Axel Rauschmayer (14 years ago)

There are indeed a few ways around this: Naming the prototype, using an array with a single element, etc.

# David Bruant (14 years ago)

Here is a code snippet:

var p = {}; (function(){ var a; Object.defineProperty(p, 'a', {get:function(){return a;}, set:function(v){a = v;}}); })();

var o1 = Object.create(p); var o2 = Object.create(p);

o1.a = 1; console.log(o1.a, o2.a); // 1 1 o2.a = 37; console.log(o1.a, o2.a); // 37 37

'a' is an own property neither of o1 nor o2. (Firefox may say the opposite, but I've been told, it's fixed and going to be released on FF 6 or 7 [1] [2]). a "value" (so to say) is shared among the instances.

David

[1] bugzilla.mozilla.org/show_bug.cgi?id=636989 [2] bugzilla.mozilla.org/show_bug.cgi?id=637994

Le 23/06/2011 13:19, David Bruant a écrit :

# Brendan Eich (14 years ago)

Adding := for a rare and usually wrong operation is not going to happen.

Let's please focus on real problems, especially when proposing new syntax.

# Brendan Eich (14 years ago)

On Jun 23, 2011, at 4:54 AM, Axel Rauschmayer wrote:

How about this? You have a bunch of local variables in various functions referencing different objects. But by some obscure yet legal means you use one such reference to mutate the prototype object they share in common, so all the others appear to be mutated too.

This is not inherently "evil", but it's usually a bug. Usually the other different objects do not wish to be mutated so indirectly, and the users of their properties (own and delegated) in those various functions may not be prepared for such a change.

So you are mainly worried about mutating methods?

No, any data property.

My main concern is encapsulation: If you have class methods, I would want to give them the option of having class state. I want the same rules that hold for the object level (state + behavior) to hold for the meta-object level.

The prototype is not the class.

Consider monkey-patching. You want to add value to a shared prototype, but you might break for-in loops. Ok, that can be solved with ES5 Object.defineProperty -- but not if the mutation breaks some other invariant.

I would probably never use for-in loops in ES.next.

That does not address the general problem. Please read the whole sentence including the part I wrote after the "--".