Tuesday, March 26, 2013

5 Reasons You Should Avoid __proto__

Update: when you've done with this post, there's even more in comments and the newer one: Yet Another Reason To Drop __proto__
Too many discussions without real outcome about __proto__ magic evilness or feature. It's time to understand why using it is, today, a bad idea.

__proto__ Is NOT Standard (yet)

TC39 refused to standardize this property because of the amount of problems it brings. Apparently will be part of ES6 but is not there yet.
__proto__ is a silent, non spec'd, agreement. What's the problem? Keep reading ;)

__proto__ Could NOT Be There

The silent non standard agreement sees __proto__ as configurable property of the Object.prototype.
As example, try this in some environment:
(this.alert || console.warn)(
  delete Object.prototype.__proto__
); // false or true ?
The outcome is migrating from false to true. As example, current Chrome has a non configurable descriptor, while Canary has a configurable one. Same is for latest node.js, Firefox, and who know who else.
Having a property configurable means you cannot trust it's going to work because anyone could have decided to remove it ... why ...

__proto__ Makes null Objects Unpredictables

Theoretically, this is all you need to create one or more objects that inherits from null
var Dict = Object.create.bind(Object, null);

var dictionary = Dict();
dictionary[property] = 1;
if (otherProperty in dictionary) {
  // do stuff
}
// or
if (dictionary[someOtherThing]) {
  // do stuff
}
We cannot trust, as example in Chrome, that any property can be set there because if for some reason the property name is __proto__, that object inheriting null will completely be unusable/screwed.
Using hasOwnPropertyDescriptor() is not even an option since objects that inherit from null do not inherit these methods from Object.prototype.
The extra silent agreement here is that Object.create(null) should return objects that are not affected anyhow by any property from Object.prototype and __proto__ ain't any exception!

__proto__ Is NOT Secure

Since any bloody object could be modified directly or deeply anywhere in the middle of its prototypal chain, you can consider all your instances somehow exposed to any sort of attack.
Bad news is: you cannot redefine __proto__ to prevent this, at least you cannot in current version of Chrome and mobile browsers:
function ProtoSafe() {}
Object.defineProperty(
  ProtoSafe.prototype,
  '__proto__',
  {
    get: function () {
      return ProtoSafe.prototype;
    },
    set: function () {
      throw 'immutable';
    }
  }
);
Error: cannot redefine property __proto__

__proto__ Influences Your Logic

This is last point but actually still important. In ES3 it has never been possible to redefine inheritance over an already created instance and this has never been a real problem.
Polymorphism has always been possible through mixins and borrowed methods but again, no way an object could suddenly be any sort of instance with such lightweight casting unable to ensure any desired behavior.
What I mean, is that using [].slice.call(arguments) has a meaning: if there is a length in the used object, create an array with indexes filled from 0 to that length - 1.
This is different from generic.__proto__ = Array.prototype; because the length could be missing and the resulted object behavior be unexpected, broken, or again unpredictable.

Still A Cheap Way To Cast

This is the only advantage and the reason some library adopted __proto__ without even thinking about it: performance boost, compared to a whole slice, are a win, and specially in mobile and DOM libraries where results are always threat as ArrayLike objects.

Object.setPrototypeOf(object, proto) To The Rescue!

In ES5 all Object things are managed through the Object constructor so why not having a method in charge of the __proto__ behavior, since Object.getPrototypeOf(object) is already available?
Here some advantage:
  1. no way a property can destroy an object, the obj[propName] check against propName !== '__proto__' won't be needed anymore: performance!
  2. cheap casting still available so not a performance issue
  3. standardizing Object.setPrototypeOf(object, proto) can bring new possibilities such Object.freezePrototype(object) in order to ensure immutable inheritance when and if needed

A Cheaper Example

If you want to ensure TC39 will ever consider to drop this property in favor of better methods in the Object, here what you should stick in your library that is using proto:
var setPrototypeOf = Object.setPrototypeOf || function(o, p){
  o.__proto__ = p;
  return o;
};
It's cheap and performance, again, are as good as before!

9 comments:

medikoo said...

Current stand of TC39 is that __proto__ will standardized and Object.setPrototypeOf is no go.
As long as that remains unchanged, adding setPrototypeOf to our code is just adding not standard mess, I wouldn't advocate that.

Maybe if there would be more of us, pushing for setPrototypeOf, something would happen, at least we all feel, that with some effort it can be done. Reasons against do not sound as real blockers.

Marcus Pope said...

Hey Andrea, generally we hold the same perspective on JavaScript, but in this particular case I think you're points aren't as strong as usual. And really I truly respect your opinions on most of your writing even when I disagree with them :D But here's my perspective:

For point 1, being non-standard is not a reason to not use something, otherwise we'd never use any of the modern features added to browsers before they become standard. As long as you don't need to rely on complete cross browser compatibility, using non standard features is fair game.

For point #2, this is the same weak argument as suggesting that you shouldn't compare against undefined because someone could redefine undefined. If you, or someone else, deletes the __proto__ property, they best know what they're doing otheriwse yeah they'll run into a problem. But that premise applies to programming as a whole, so...

#3 - "__proto__" only makes objects unpredictable when you are trying to use properties named "__proto__". It doesn't make the entire object "unusable/screwed" just that property on the object. And like most dynamic languages there are general guidelines that developers agree to - a social contract if you will. Other agreements exist like not redeclaring undefined, that prevent such issues from happening. This is a reason to not use "__proto__" as an object property, in the same way you shouldn't use "length", "toString" or "hasOwnProperty" strings as key values of an object. Those could just as easily make code behave unpredictably, but you wouldn't recommend avoiding objects in JavaScript.

#4 - Security in JavaScript is almost a false premise altogether. The only really immutable mechanism in JS is to use privately scoped variables and to rely on local properties of those privately scoped variables. Even still there are ways around that, frozen objects, immutable objects etc. Even your example of redefining __proto__ as an 'immutable' property doesn't make ProtoSafe safe from prototype changes. I could just as easily serialize or mock up a comparable object that allows for __proto__ modifications and inject that at key points to alter code interpretation. If its usage is truly private, I could use a man in the middle attack and feed manipulated JavaScript into the browser across the wire.

#5 - redefining inheritance over an already created instance is not a problem - it's just another way of doing polymorphism. Just because other (more complex) means exist to recreate the end result doesn't mean this method is inherently flawed. This notion that if you did something like "generic.__proto__ = Array.prototype" could lead to unpredictable/broken behavior and therefore you should never use "__proto__" is just crazy. Especially since "if (generic.length) { generic.__proto__ = Array.prototype } else { throw new Error(); }" fixes that problem entirely.

And since ES6 is introducing the property as a standards compliant interface there's really not much weight to any of these points. :( Personally I'm happy to see it be added as a standard property since I've personally avoided it's use for years since it wasn't fully cross browser compatible. And IE11 is finally changing that situation.

Andrea Giammarchi said...

@medikoo
If libraries that use __proto__ will simply pass through my latter snippet, engines and TC39 can think about adoption of that standard.
We, as edevelopers, should not suffer their decision but rather propose them via daily usage.
__proto__ can go, is a mistake, and it should never be part of the spec, same mistake and security problem than __parent__, imho, and as it is now, fn.caller too making hidden classes impossible to create.

@Marcus
point 1: I have proposed a non standard and better way to solve the issue. The reason __proto__ is being adopted is just because it's a de-facto standard and they believe there's no way to drop it. Well, since they decided to make it configurable, it's actually the other way round, it's really easy to delete it. My proposal holds once the original descriptor and resolve the problem removing it from the Object.prototype.
If all of us will use similar approach, nobody would rely on Object.#__proto__ and we can define the next "de-facto" standard.

point 2: in ES3 if you (function(key){this[key] = true}()) or any similar situation you basically redefine global window.undefined as true by accident. Is not that you explicitly set that, is the reason "use strict" avoid this as global object: security!
Same here with __proto__, if by accident that key is the string '__proto__' you, your code, your app, is screwed!!! To cast objects vi __proto__ is way too easy and not that easy to control. The reason we have to obj.hasOwnProperty(key) every bloody for/in loop will be our new nightmare: be sure that key !== '__proto__' every time we interact with dynamic strings for dictionaries or objects. You see? obj[shenanigan] is way easier than an explicit operation as Object.setPrototypeOf(generic, proto) is ... there is no way you can mess up that in a loop of strings, as example.

point 3: answered via point 2, the elephant in the room, the repeated mistake, nobody seems to spot

point 4: the reason __proto__ cannot be dropped is SES, the "secure" version of JavaScript used probably only inside Google ... so, if security in their opinion is what makes __proto__ impossible to drop, security is considered even too much in modern ES6 specifications. I agree with you security in JS is partial utopia, but then a JS world without __proto__ is way more secure than it is now. Not all of us use that SES thingy ...

point 5: length is not necessarily an integer but I understand your point. Mine is that JSObject.__proto__ = HTMLElement.prototype is, today, a non sense ... A mechanism to understand if a prototype can be implemented (Object.preventExtend(C.prototype)) or a mechanism to avoid that possibility (Object.freezePrototype(JSObject)) are much better/suitable ways to do things accordingly, and consistently, with ES5 direction which was good!!!

Once again, ES5 was all about avoiding Object.prototype pollution and __proto__ is the most destructive thing ever they could silently let it be and it lives in the Object.prototype.

I do hope __proto__ will never make it, because there are no real points in favor of it while there are more than 5 points against it.

Marcus Pope said...

preface: I really wish there was a better method of discussing topics like this, on blogs in 2013 :(

If I understand you properly, a big part of your dissatisfaction with __proto__ is that you would have rather seen it implemented as a function on the Object namespace, or some other manner, instead of using a key on the prototype chain. Had we done Object.setProto(some_obj, Array.prototype); this would have mitigated a big pain point from your perspective correct?

I can understand that from a philosophical perspective, but from a pragmatic perspective I don't see any real consequence. I don't see any likelihood of namespace collision from a developer choice given the history of the variable. And from a malevolent perspective, there is nothing you can do with __proto__ that you couldn't also do with ES3 as it stands today (from what I could think of, I would concede to any concrete example of course.) But since __proto__ does exist today I think history has proven malevolence on this path is not really a concern.

On point #2, the nightmare you envision isn't really a necessary consequence either, or at least it's a consequence that is easily overcome with one reusable iterator function. I haven't written a new hasOwnProperty call in recent years because I use a functional iterator that has a single hasOwnProperty check. Everywhere else I use the iterator to avoid ever having a need to type a lengthy for statement or an extra if block. The same could be achieved with resetting an __proto__ value from a cached version before iteration and restored after iteration (or de/serialization, or whatever.) This obviously isn't ideal or remotely close to philosophically pure, but it is a way to avoid the disaster you mention, all while saving people from hasOwnProperty mistakes too. So I consider __proto__ and hasOwnProperty to be of equal danger and of equal ease to solve.

#4 - I disagree that there is any significant security delta between the two worlds. And a false-premise of security is no different than a better false sense of security - they are both insecure. But an example would be convincing on this point too.

As for point #5, I agree with you that implementing __proto__ in es5 after all the effort to avoid prototype namespace pollution is a pretty big miss from a future language spec perspective -- and I agree that this will likely seal it's place, or create another painful side step towards progress. But I'm not a big fan of most of es5 and see enough setbacks that this decision doesn't surprise me. ES5 looks like a grade schooler stuck a bunch of shiny stickers on a roughed out sculpture of a masterpiece instead of polishing up the masterpiece - though we'd need to have a skype conversation to cover that difference of opinion because this is just the wrong discussion format, even probably for this conversation too!

I would personally love to have read/write access to the live prototype chain of instantiated objects - not for arguments slicing but for runtime polymorphism that can rely on dependency injection instead of explicit mixins. Just as I would love it if everyone used hasOwnProperty because I like using the same construct for iteration as is used for reflection. If anything I would like to see more runtime flexibility in the language as I think this would get us closer to an impervious language.

Andrea Giammarchi said...

you can have that access via Object.getProottypeOf(), already available in ES5, and Object.setPrototypeOf() which is my proposal. __proto__ is just a mistake, the fact could be useful, no doubts, the facts I mentioned it's problematic as it is ... no doubts neither!

Quildreen "Sorella" Motta said...

> This is last point but actually still important. In ES3 it has never been possible to redefine inheritance over an already created instance and this has never been a real problem.

IMHO, the whole point of prototypical inheritance (all the more delegative inheritance) is that you can just change the delegation around whenever you want. That concept happens to be not as powerful in JavaScript because the language doesn't have multiple delegation.

But think about this scenario instead:

function asCollection(as) {
return isArray(as)? as
: isSequence(as)? { __proto__: as }
: /* otherwise */ [as]
}

One paper I've read that goes deeper into mutable prototypes and their uses (even though it doesn't apply to JS) is the one on Slate's implementation of multiple dispatching with multiple prototypes: http://files.slatelanguage.org/doc/pmd/ecoop.pdf

Andrea Giammarchi said...

you can indeed change runtime all methods and properties without even needing inheritance or for all instances through their proto object ... so, what's your point? Have you read what is the problem __proto__ causes? What would be wrong with:

function asCollection(as) {
return isArray(as)? as
: isSequence(as)? Object.setPrototypeOf({}, as)
: /* otherwise */ [as]
}

Is exactly the same except is safer, explicit, you don't write that by accident, etc etc ... which is what I am suggesting here!

Marcus Pope said...

Just a thought, would differentiating language features like hasOwnProperty and __proto__ so that they are only accessible via dot notation/iteration, and then giving bracket notation/serialization exclusive access to the local properties only, be a feasible (but clearly not backwards compatible) means of solving the problem?

{}.__proto__ === Object.prototype;
{}['__proto__'] === undefined;

Serialization would only ever set values via the bracket "layer". You could "protect" your code by only ever referring to the bracket layer. Introspection via for-loop iteration could safely rely on x.hasOwnProperty(), and objects as hashes would have zero namespace conflicts with any key value.

You could probably even let dot notation check the bracket layer first for overrides, before opting for the prototype layer. And although dot notation doesn't allow for variable references, you could easily access prototype methods dynamically by referencing the prototype object as a local hash via dot notation:

var attr = 'hasOwnProperty';
{}.prototype[attr]();

I personally love the readability and terseness of syntax chaining versus the recursive backwards nature of nested function calls.

[].every().filter().sum();

vs

Array.sum(Array.filter(Array.every([])));

(code purposely redacted to make a point)

Although there are prettier ways to write the latter, chaining is still a cleaner approach. It seems like this would answer all of your concerns about __proto__ and other special prototype properties - except for your points about giving developers enough rope to hang themselves when they reassign a prototype inappropriately.

Andrea Giammarchi said...

back to my steps, how easier and problem fre woul dbe NOT having __proto__ at all ? ...