David Bruant wrote:
> Le 20/03/2013 16:36, Nathan Wall a écrit :
>> I didn't get a direct response to my question about mutating proto on 
>> objects which don't inherit from Object.prototype, but I'm inferring from 
>> [1] that it won't be possible. I find this unfortunate, but I realize this 
>> issue has seen a lot of discussion in the past and there are reasons for the 
>> current decision. I will see how I can make my code cope with reality.
> Could you describe how you use __proto__ on objects not inheriting from
> Object.prototype?
>
> From what I know there are 2 main use cases:
> 1) object as map
> changing the prototype enable changing different "default values". I
> guess any solution to that problem either looses the object syntax
> (maybe unless using proxies) like using an ES6 Map or has non-trivial
> runtime cost.
> Or the code needs to be reorganized so that the object is always created
> after the prototype (using Object.create for instance)
>
> 2) Subclassing
> ES6 will have classes with inheritance. That's mostly syntax sugar on
> top of what's already possible, but that works.
>
> Do you have a use case that belongs in neither of these categories?

Hi David.

I would add (3) Integrity/Security. When you don't want mucking with 
`Object.prototype` to be able to influence a script.

My current use-case has to do with shimming private properties into ES5. It may 
be something I want to continue doing in ES6 depending on how private 
properties are implemented in ES6.  The full working library is available at 
https://github.com/Nathan-Wall/Secrets

Let me explain the relevant portions for why I want mutable proto on objects 
which don't inherit from Object.prototype.

Take this simple, highly contrived example:

    var apple, grannySmith;
    (function() {
    
        var priv = createSecret();

        apple = {
            get foodType() { return priv(this).foodType; },
            get color() { return priv(this).color; },
            toString: function() { return this.color + ' ' + this.foodType; }
        };

        priv(apple).foodType = 'fruit';
        priv(apple).color = 'red';

        grannySmith = Object.create(apple);

        priv(grannySmith).color = 'green';
    
    })();

    apple.toString();       // => 'red fruit'
    grannySmith.toString(); // => 'green fruit'

In ES5 Secrets works by overriding `Object.getOwnPropertyNames` to hide a 
secret property. In ES6, this could work using `makePrivate`, WeakMaps, or 
whatever mechanisms are available to create private state.

`priv` is a function which returns an object which represents the private state 
of any object.

The important part is that in order for the above to work, secrets need to 
respect prototypal inheritance.  When `grannySmith.toString` is called, it 
calls `grannySmith.foodType`, which accesses the "private" `foodType` property. 
Since `grannySmith` doesn't have a `foodType` property on its private object, 
it should look up the inheritance chain for `apple`'s `foodType` private 
property.

The following code snip shows how this is set up for the non-mutable proto case:

(In the following code `create = Object.create` and `getPrototypeOf = 
Object.getPrototypeOf`.)

    function createSecret() {

        var id = nextUniqueId();

        return function secret(obj) {
            var S, proto, protoS, protoSTest,
                // This function generates an object which is stored on a 
hidden property of `obj`.
                secrets = Secrets(obj)
            if (secrets) {
                // The id is used to distinguish between different secret 
functions.
                S = secrets[id];
                if (!S) {
                    proto = getPrototypeOf(obj);
                    // Create a new object which inherits from the secret of 
obj's prototype.
                    secrets[id] = S = create(proto ? secret(proto) : null);
                }
                return S;
            } else
                // The object may have been frozen in another frame.
                throw new Error('This object doesn\'t support secrets.');
        };

    }

As you can see, the private objects inherit from other private objects which 
eventually inherit from null. For instance:

    var A = { };
    var B = Object.create(A);
    var C = Object.create(B);

    var priv = createSecret();
    
    var privC = priv(C);

C's prototype chain looks like:

    null -> Object.prototype -> A -> B -> C

so privC's prototype chain looks like:

    null -> priv(Object.prototype) -> priv(A) -> priv(B) -> priv(C)

where `priv(X)` is the private object used to store the private state of X.

In order to get this to work for mutable proto, the private objects need to 
change their prototypes whenever the public object changes its prototype.

    var D = { };
    C.__proto__ = D;
    // C's new prototype chain:
    //    null -> Object.prototype -> D -> C

So we need to change priv(C)'s prototype chain to match.

But priv(C) should not inherit from Object.prototype for two reasons. (1) 
Because it should really inherit from priv(Object.prototype), the private 
object which represents the private state of Object.prototype, and (2) for 
security reasons, so that someone can't define a setter on Object.prototype to 
retrieve properties that are intended to be private.  You shouldn't be able to 
set properties on priv(Object.prototype) unless you have access to the `priv` 
function.

Going back to the apple example, the following is a potential attack if 
priv(apple) inherits from Object.prototype:

    Object.defineProperty(Object.prototype, 'foodType', {
        set: function(value) {
            tellTheWorld('Setting foodType to ' + value);
        }
    });

Here's an update which attempts to solve this for mutable proto.

The following code references `protoIsMutable` which is either `true` or 
`false` depending on whether mutable __proto__ has been detected in this 
environment. `setPrototypeOf` needs to be available; a running implementation 
is shown under the `createSecret` function.

    function createSecret() {

        var id = nextUniqueId();

        return function secret(obj) {
            var secrets = Secrets(obj),
                S, proto, protoS, protoSTest;
            if (secrets) {
                S = secrets[id];
                if (!S) {
                    proto = getPrototypeOf(obj);
                    secrets[id] = S = create(proto ? secret(proto) : null);
                } else if (protoIsMutable) {
                    proto = getPrototypeOf(obj);
                    protoS = getPrototypeOf(S);
                    protoSTest = proto == null ? null : secret(proto);
                    if (protoSTest !== protoS)
                        setPrototypeOf(S, protoSTest);
                }
                return S;
            } else
                // The object may have been frozen in another frame.
                throw new Error('This object doesn\'t support secrets.');
        };

    }

    var setPrototypeOf = (function() {

        if (!protoIsMutable)
            return;

        var _setProto = Object.getOwnPropertyDescriptor(Object.prototype, 
'__proto__').set;
        if (_setProto)
            return Function.prototype.call.bind(_setProto);

        // If the implementation supports mutable proto but doesn't have a 
__proto__ setter, see if
        // mutable proto is possible on objects which don't inherit from 
Object.prototype.
        // This behavior has been observed on Chrome 25 but is believed to be 
fixed on a modern V8.
        // https://mail.mozilla.org/pipermail/es-discuss/2013-March/029244.html
        // However, the version of V8 mentioned in the above post does not 
support __proto__ setter.
        // It is thought that this version of V8 will not support mutable proto 
on objects which
        // don't inherit from Object.prototype.
        // It is also currently unknown which direction the spec will go on 
this issue.
        var A = Object.create(null),
            B = Object.create(null);
        A.test = 5;
        B.__proto__ = A;
        if (B.test == 5)
            return function(obj, proto) {
                obj.__proto__ = proto;
            };

        // Unfortunately, it sounds like ES6 may be on course for this route.
        return function() {
            throw new Error(
                'Mutable prototype is supported by this implementation, but it 
does not support mutating the prototype '
                + 'of an object which doesn\'t inherit from Object.prototype'
            );
        };

    })();

This updated version will fail, however, for implementations which support 
mutable proto but don't support mutating proto on objects which don't inherit 
from `Object.prototype`.

Nathan                                    
_______________________________________________
es-discuss mailing list
es-discuss@mozilla.org
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to