Property descriptors as ES6 Maps

# David Bruant (11 years ago)

I've recently filed a spec bug [1] and given more thoughts about it that goes beyond the suggested restructuring so I'm bringing it up here. This posts ends up with an unresolved issue, but I hope a solution can be found.

Let's talk about property descriptors. Currently, in the spec, property descriptors have their own type (with "fields" and accessing things in these fields). It's a type that matches very well with objects, so the mapping with Object.defineProperty input and Object.getOwnPropertyDescriptor is rather obvious. However, when we think about proxy traps, the proxy traps can't have access to an abstract data structure, so the proxy spec uses regular objects for trap communication.

My bug was about making the use of objects official in the spec internals... until I realized that ES6 has maps. I'm writing to propose a change which is to make property descriptor ES6 maps both inside the traps and to the external interface. There is no problem for traps (except maybe that it changes the current Firefox implementation...). A backward compatibility may exist for ES5 code:

  • Object.defineProperty using maps => For legacy reason, it would keep accepting plain old objects, but

when passed a map, it would use the map get/set/has protocol.

  • Object.getOwnPropertyDescriptor returning maps => That part is a bit trickier, because current code has a ridiculous

amount of 'desc.configurable', 'desc.enumerable', etc. in it and this must keep working. I'm not sure I have found an acceptable idea for this part. One major issue I see is the naming conflict between Map.prototype.get/set and the 'get' and 'set' attributes of property descriptors.

In the hope it will inspire someone who'll be able to find a solution.

David

[1] ecmascript#863 [2] harmony:proxies_spec

# Andreas Rossberg (11 years ago)

On 31 October 2012 10:40, David Bruant <bruant.d at gmail.com> wrote:

My bug was about making the use of objects [for property descriptors] official in the spec internals... until I realized that ES6 has maps.

Can you motivate why maps would be more adequate? Frankly, I completely disagree, because they would have a highly heterogeneous type.

# David Bruant (11 years ago)

Le 31/10/2012 12:46, Andreas Rossberg a écrit :

On 31 October 2012 10:40, David Bruant <bruant.d at gmail.com> wrote:

My bug was about making the use of objects [for property descriptors] official in the spec internals... until I realized that ES6 has maps. Can you motivate why maps would be more adequate?

For "more adequate than object", I would say that they are not concerned with pseudo-properties of JS-in-reality (proto, defineGetter, ...) which may make a difference in traps.

Frankly, I completely disagree, because they would have a highly heterogeneous type.

Another idea is to define and expose a specific PropertyDescriptor type (that will be manipulated by proxy traps). I would prefer that solution actually, but I'm afraid backward compat may prevent that from happening. It would be a great occasion to expose a bunch of PropertyDescriptor.* functions to manipulate them. For instance, with proxies, I've had to implement some algorithms to merge descriptors (basically merge what I want to return with invariant-enforceable parts of the target descriptor) and this code belongs to the standard library, not application code.

# Allen Wirfs-Brock (11 years ago)

Let me summarize what I think is your concern.

In ES5, property descriptor records are a specification device that is used to transport information about object properties between factored components of the ES specification. The same information can be expressed as an ES object that is produced/consumed by Object.getOwnPropertyDescriptor and Object.defineProperty. These are raising/lowering operations that move information from the specification/implication level to the reflective ES language level. One advantage of this layering is that the conversion to/from an actual object takes place once, at a well defined point in the execution sequence and any side-effects of object access occur only at that point. Once the information is represented as an internal "record" we know that all internal consistency preconditions are satisfied and that no side-effects can be associated with access to such records.

You concern seems to be that a proxy traps that deal with descriptor objects have no such guarantees and in particular strange things might happen if a descriptor object is itself a proxy.

I don't think any of this is actually a problem. First there are only a few traps that directly deal with descriptor objects.

The getOwnPropertyDescriptor trap is a lowering operation. It is called by a proxy's [[GetOwnProperty]] internal method implementation. The internal method expects to get back a property descriptor object which it must then lower (via the ToPropertyDescriptor abstract operation) into a descriptor record. ToPropertyDescriptor can throw if the descriptor object being lowered is malformed or if it misbehaves (via accessor properties or by being a Proxy itself) but that is ok because throwing is part of the specified behavior of ToPropertyDescriptor and once we are past that we know we have a well-formed internal descriptor record that is not subject to tampering. Keeping the original object around would infect the entire downstream use of the property descriptor with the possibility of side-effects.

The defineProperty trap is a raising operation. It is called by a Proxy's [[DefineOwnProperty]] internal method and starts with an internal descriptor record. The internal method converts the record to an object (via the FromPropertyDescriptor abstract operation) and passes that object to the trap. Note that this is guaranteed to be a ordinary object (not a proxy) and that the properties that describe property attributes are guaranteed to be data properties. So, defineProperty traps don't need to worry about getting passed a bogus property descriptor object as an argument.

The other possible concern would be that a trap might directly use Object.getOwnPropertyDescriptor on a proxy object and that this might provide a bogus descriptor. But it can't. Object.getOwnPropertyDescriptor may trigger a getOwnPropertyDescriptor trap but as described above this is a lowering operation that produces a well formed property descriptor record (or throws if it can not). That lowered record is immediately raised via FromPropertyDescriptor, so we know that Object.getOwnPropertyDescriptor always returns an ordinary object that is a well formed, side-effect free property descriptor object.

so, I just don't see any basis for your concern. Perhaps, you could elaborate on the nature of the problem as you perceive it.

# Tom Van Cutsem (11 years ago)

David, I think I see where you are going: property descriptors are basically "bags" of key/value properties, and maps are a more direct representation of this concept than objects.

On the other hand, as so carefully explained by Allen, there currently isn't really an issue with the mapping between property descriptors and objects: at all the boundary points, we make sure to properly convert descriptors into well-behaved objects and vice versa.

As you point out yourself, making the change from objects to maps implies a bunch of backwards compat. issues. My position is that doing the object->map refactoring at this stage would entail a lot of work for very

little gain.

Finally, remember that property descriptors-as-objects really leverage the object literal notation (i.e. I can create a property descriptor by just writing "{value:42,writable:true}". I don't think there is a corresponding sweet syntax for literal maps? In any case, Object.defineProperty will need to continue accepting such literal objects as its third argument, so why not keep the story simple and not widen the type to object | map.

Cheers, Tom

2012/10/31 Allen Wirfs-Brock <allen at wirfs-brock.com>

# Axel Rauschmayer (11 years ago)

(Assuming that I understand the issue.)

The "object to map" refactoring matters whenever you don't know the keys in advance. If you do, as is the case with property descriptors, objects are fine. Then they are more like records than like maps.

Axel

[[[Sent from a mobile device. Please forgive brevity and typos.]]]

Dr. Axel Rauschmayer axel at rauschma.de Home: rauschma.de Blog: 2ality.com

# David Bruant (11 years ago)

Le 01/11/2012 10:37, Tom Van Cutsem a écrit :

On the other hand, as so carefully explained by Allen, there currently isn't really an issue with the mapping between property descriptors and objects: at all the boundary points, we make sure to properly convert descriptors into well-behaved objects and vice versa.

Indeed. What do you think of the idea of exposing objects that would be well-behaved as property descriptor by construction? These objects can be used both internally and at trap boundaries. It wouldn't be compulsory to preserve backward compat, but just more efficient.

The name I've used in my answer to Allen is awful, but it can obviously be changed :-) (maybe PropDesc or PropertyDescriptor?)

As you point out yourself, making the change from objects to maps implies a bunch of backwards compat. issues. My position is that doing the object->map refactoring at this stage would entail a lot of work for very little gain.

I agree.

Finally, remember that property descriptors-as-objects really leverage the object literal notation (i.e. I can create a property descriptor by just writing "{value:42,writable:true}". I don't think there is a corresponding sweet syntax for literal maps?

I don't think maps are a good idea any longer, but for this point, I think the following has been suggested: Map({value:42, writable:true}) (If it hasn't yet, it sounds like a fanstastic idea to initialize ES6 maps!)

The constructor I have proposed could have an equivalent initialization syntax. Object.defineProperty({}, 'a', PropDesc({value:42, writable:true}))

# Brandon Benvie (11 years ago)

Another argument against using maps is it can actually be pretty useful to use the features that prototypal inheritance and the object model provides.

function Descriptor(configurable, enumerable){ this.configurable = configurable; this.enumerable = enumerable; }

Descriptor.prototype = { configurable: undefined, enumerable: undefined, inerit: function inerit(){ return Object.create(this); } };

function DataDescriptor(value, writable, enumerable, configurable){ this.value = value; this.writable = writable; this.enumerable = enumerable; this.configurable = configurable; } DataDescriptor.prototype = new Descriptor DataDescriptor.prototype.value = undefined; DataDescriptor.prototype.writable = undefined;

function AccessorDescriptor(get, set, enumerable, configurable){ this.get = get; this.set = set; this.enumerable = enumerable; this.configurable = configurable; } AccessorDescriptor.prototype = new Descriptor; AccessorDescriptor.prototype.get = undefined; AccessorDescriptor.prototype.set = undefined;

function Value(value){ this.value = value; } Value.prototype = new DataDescriptor(undefined, true, true, true);

function HiddenValue(value){ this.value = value; } Value.prototype = new DataDescriptor(undefined, true, false, true);

function Accessor(get, set){ this.get = get; this.set = set; } Accessor.prototype = new AccessorDescriptor(undefined, undefined, true, true);

function Getter(get){ this.get = get; } Getter.prototype = new Accessor;

function Setter(set){ this.set = set; } Setter.prototype = new Accessor;

# Tom Van Cutsem (11 years ago)

2012/11/1 David Bruant <bruant.d at gmail.com>

The constructor I have proposed could have an equivalent initialization syntax. Object.defineProperty({}, 'a', PropDesc({value:42, writable:true}))

I see the merit in your proposal as making the Object->PropDesc conversion

explicit. However, I think it's too late for ES6: Object.defineProperty must continue to accept Objects as its third argument, and that's even shorter to write. So even if we would add such a PropDesc constructor, I don't think most developers (including myself) would adapt.

# Andrea Giammarchi (11 years ago)

I would add ... am I the only one that does not create a new object per each defined property ? I am recycling descriptor.value like hell, I wonder if anyone else out there is doing the same.

# Allen Wirfs-Brock (11 years ago)

On Nov 2, 2012, at 9:29 AM, Andrea Giammarchi wrote:

I would add ... am I the only one that does not create a new object per each defined property ? I am recycling descriptor.value like hell, I wonder if anyone else out there is doing the same.

Don't know what people are actually doing, but that usage mode was always envisioned when we designed property descriptor objects.

# Andrea Giammarchi (11 years ago)

I've asked because I use this pattern quite a lot

Object.defineProperty({}, 'a', propValue(42))

function propValue(value) { // genericDescriptor define elsewhere genericDescriptor.value = value; return genericDescriptor; }

obviously when it makes sense to reuse the descriptor. I define almost everything through this pattern that when I've seen an extra constructor around I've freaked out :D