Mutable Proto

Nathan Wall nathan.wall at live.com
Wed Mar 20 13:01:09 PDT 2013


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 		 	   		  


More information about the es-discuss mailing list