Feedback on "Read-only Override Prohibition is [not a mistake]"

Allen Wirfs-Brock allen at wirfs-brock.com
Sun Jan 15 11:21:43 PST 2012


On Jan 15, 2012, at 8:10 AM, David Bruant wrote:

> Hi,
> 
> I'm refering to Allen's writing at [1]:
>> 
>> The basic idea is that the properties prototype object are shared
>> parts of all of inheriting child object. Modifying such a shared part
>> by a child, introduces a local change that is visible to that child
>> (and its children) so this requires creation of a “own” property on
>> the child. However, read-only properties can not modified (by normal
>> means, eg assignment) so there is no need to create a “own” copy.
>> 
> I agree with what you describe as an intention, but I don't know to what
> extent it applies to JavaScript (I don't know Self unfortunately):
> -----
> var p = {};
> var o = Object.create(p);
> var o2 = Object.create(p);
> o.a = 1; // creating an own property
> 
> Object.defineProperty(p, 'a', {value:2, configurable:false,
> writable:false});
> // now, o and o2 "share" an 'a' property. But do they really?
> -----
> The dynamicity of JavaScript makes the notion of "sharing" dynamic as
> well, I think.

However, the above requires Object.defineProperty which did not exist prior to ES5.  Since the starting point of this conversations regards whether or a design "mistake" was made in the ES5, I don't think it is valid to use defineProperty in trying to understand the original (prior to ES5) design intent.

> 
>> Assigning to an inherited read-only property or a “own” read-only
>> property should have the same affect (whether it is ignoring the
>> assignment, throwing, etc.).
>> 
> -----
> // following above code
> o.a = 3;
> o2.a = 4;
> -----
> Due to o having an 'a' property before inheriting from a read-only one,
> the notion of "same effect" cannot be applied (unless breaking other
> invariants).
> 
> 
>> Allowing assignment to an inherited read-only property would break the
>> invariant that that a prototype’s readonly property is an immutable
>> value that is shared among all children of the prototype.
>> 
>> If there was a mistake in designing ES5, it was allowing
>> Object.defineOwnProperty to create child properties that over-ride
>> inherited read-only data properties.This broke an invariant that
>> previously existed in the language but this invariant was already
>> violated by some pre-ES5 clause 15 objects, (eg the writability of the
>> prototype property of some children of Function.prototype). However, I
>> think the ES5 decision was probably the right one given the legacy
>> clause 15 usages and the overall reflective nature of defineOwnProperty).
>> 
> If I sum up, the design of Object.defineProperty was necessary for
> backward compatibility (pre-ES5) reasons. A consequence of this design
> is breaking the invariant you mentionned ("a prototype’s readonly
> property is an immutable value that is shared among all children of the
> prototype").

It was already broken in ES3 for some library objects.  However, the invariant still held in ES3 for all user defined objects. Object.defineProperty allows ES programmer to "break" the invariant in the same manner that ES implementors had previous been allowed to.  However, ES5 maintained the exact same semantics for assignment to an inherited read-only property as had existed in ES1-3.

> Considering this invariant is broken (as i showed above, it was probably
> doomed to be broken anyway), is there still a reason to not align
> [[CanPut]] as Mark suggests?

TC39(and the web) has a low tolerance for changes that will break existing code. In general, TC39 tries to avoid changes to the observable semantics of the language as defined by previous versions unless those changes are tied to new syntactic forms or some other type of explicit opt-in.  This was true for ES5 and remains true today.  Changing the semantics of assignment to an inherited read-only property would be such a change. It wasn't made in E5 and it would seem even harder to justify the change for ES6, given that ES5 made it easier to observe the semantics.

> 
> For the record, at the last JSConf, someone showed me code he wrote
> where he was declaring data attributes on the prototype with 'null' as
> value. It was the only way for his analysis tool to understand that
> "child instances" had such and such property. Such a practice prevents
> freezing the prototype (because the constructor would fail to assign
> values (unless using Object.defineProperty)).
> I don't know whether any part of this experience is good or bad, but I
> think it's worth noting that "declaring" things on the prototype is a
> practice that exists.

Yes, of course.  And in ES5, using Object.defineProperty in the instance constructor is probably the right way to deal with this idiom.

But the above can also be viewed as an existence proof that the existing set of attributes (plus the extensible flag) is insufficient to express all reasonable usage patterns.  Rather than changing the semantics associated with an existing attribute to enable a new pattern (while disabling other currently support patterns) another approach to consider is the addition of new but backwards compatible attributes that enable new semantics.  For example, you can imagine a "duplicate in children" attributes or a ""writable via over-ride" attribute that would address such scenarios.

In ES5 we avoided new attributes by evolving dontDelete into configurable and in ES6 we want to minimize additional primitive object model changes.  However, if there are important use cases that need to be supported then new attribute combinations seem like a plausible alternative to changing existing semantics. 

Allen




More information about the es-discuss mailing list