My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Saturday, March 30, 2013

Yet Another Reason To Drop __proto__

I know it might sound boring but I really want to put everything down and laugh, or cry harder, the day TC39 will realize __proto__ was one of the most terrible mistakes.

A Simple Dictionary Attack

ES6 says that Object.create(null) should not be affected anyhow from Object.prototype.
I've already mentioned this in the 5 Reasons You Should Avoid __proto__ post but I forgot to include an example.
You can test all this code with Chrome Canary or Firefox/Nightly and the most basic thing you need to know is this:
var n = Object.create(null);
n.__proto__ = {};

for (var k in n) console.log(k); // __proto__ !!!
Object.keys(n); // ["__proto__"] !!!
Got it? So, __proto__ is enumerable in some browser, is not in some other but it will be in all future browsers. Let's go on with examples ...
// store values grouped by same key
function hanldeList(key, value) {
  if (!(key in n)) {
    n[key] = [];
  }
  n[key].push(value);
}
// the Dictionary as it is in ES6
var n = Object.create(null);
Above code simply does not need to be aware of any problems except in older environment that won't work as expected. If the key is __proto__ instead of storing the value there will be most likely an error or the object will inherit from an empty array the moment n[key] = [] will be executed.
In few words, I believe you don't want to fear security and logic problems every single time you set a property to a generic object ... am I correct?
Now imagine some library such underscore.js, has the most common and generic way to create an object from another one, copying properties ...
function copy(obj) {
  var result = {}, key;
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = obj[key];
    }
  }
  return result;
}

// or if you want ... 
function extend(a, b) {
  for (var key in b) {
    if (b.hasOwnProperty(key)) {
      a[key] = b[key];
    }
  }
  return a;
}
Now guess what happens if you would like to copy or extend that list we had before, where __proto__ will be own property for the n variable and the loop is not checking the key as it should ... the object a, or the new one, will automatically extend an Array and break completely its own expected behaviors ^_^
This is nothing an explicit Object.setPrototypeOf() could cause ... moreover...

Real World Performance Impact

Every utility such lo-dash or underscore should now do this kind of check per each loop if these would like to be considered safe:
function copy(obj) {
  var result = {}, key;
  for (key in obj) {
    if (
      obj.hasOwnProperty(key) &&
      key !== '__proto__'
    ) {
      result[key] = obj[key];
    }
  }
  return result;
}
Now try to investigate in your real-world daily code how many times you change __proto__ compared with how many times you loop over properties ... I give you a test case to compare performance and remember: mobile matters!

Really Hard To Debug

Being a special property in the Object.prototype and not just a function you could wrap and keep under control, in a new scenario where any object could be instanceof anything at any time, the inability to intercept __proto__ calls and changes before it happens will be a painful experience in terms of debugging ... what was that instance before? Most likely, you'll never know ^_^
It must be said some engine makes that descriptor.setter reusable but this is not the case of current V8, as example, neither the case for all mobile browsers out there today.

A Stubborn Decision

What's driving me crazy about this property and all problems it brings, is that regardless there is a possible "speccable" Object.setPrototypeOf() alternative that would not suffer from anything I've described in all these posts, and just as reminder there is already a spec'd and widely available Object.getPrototypeOf() in ES5, TC39 will go on and make the problem a standardized one ^_^
I haven't been able to reason against them regardless examples and reasons ... but you could have fun trying too before it's too late!

8 comments:

LJHarb said...

Can you demonstrate special "__proto__" behavior on an Object.create(null)? Based on your example, it just looks like you're setting a dictionary key no different from "foo" - which of course should be enumerable.

The danger would be if setting "__proto__" on an Object.create(null) actually imbued it with a prototype.

Unknown said...

>>I know it might sound boring but I really want to put everything down and laugh, or cry harder, the day TC39 will realize __proto__ was one of the most terrible mistakes.

__proto__ wasn't TC39's mistake and everybody who participates on TC39 is aware of how terrible it is. However, the web (particularly the mobile web) demands it and the holdout browsers that have never previously implemented it have been forced to provide it in order to remain competitive in the mobile space.

TC39 can't force browsers to not implement an extension such as __proto__. The best we can do is provide a specification that will lead to uniformity of __proto__ behavior in all browsers.

If you want to eliminate __proto__ you will have to eliminate its usage. Write a shim for Object.setPrototypeOf:

Object.setPrototypeOf = function(obj, proto) {
obj.__proto__ = proto;
return obj;
}

Then evangelize web developers like crazy to update all their existing deployed code to use this shim instead of directly using __proto__.

If, within 6 months from now, you have eliminated substantially all of the direct usage of __proto__ then we could probably exclude it from ES6 and provide setPrototypeOf instead.

Good luck (seriously),
Allen

Andrea Giammarchi said...

LJHarb that's exactly the danger problem...when you have generic methods to copy between two objects you don't know if these are __proto__ sensitive.


Allen, thanks, I m doing it already but I don't understand why you guys cannot be pro positive and say that __proto__ should not be standard. The shim, the better one, in the first link, my other post

Unknown said...

>>I don't understand why you guys cannot be pro positive and say that __proto__ should not be standard.

Because TC39 doesn't believe that browsers will actually remove it or that code that now depends upon it will be updated.

And if it is going to be in browsers, it needs to have a standardized semantics so that future developers can rationally work with it.

Andrea Giammarchi said...

it took nothing for you to write that shim, of course as soon as there is a standard Object.setPrototypeOf() library will update, is really easy/straightforward ... that's why I've called that "a stubborn decision", looks like chickens or eggs first problem to me.

tobi said...

I totaly agree.
The TC39 wants to eliminate "with", why not the "__proto__"-mistake which is not yet a standard?!

Another, not so relevant reason: It is ugly

tobi said...

inst = Object.create(null);
inst.__proto__ = Array.prototype;
document.write(inst.forEach); // firefox:undefined, webkit:[native code]

@webkit: I want a "clean" (secure) Object , no magic __proto__ property!!!
@Firefox/TC39 : Ok, but nevertheless i want be able to change the prototype-chain

The only Solution i see is Object.setPrototypeOf();

Unknown said...

@tobi

Your example is actually demonstrates why there is a need to standardize the semantics of __proto__

It's actually webkit's support of __proto__ (in particularly on iOS but also standard Android browser) that resurrected it and that is forcing MSFT to support it for the first time. No way for any browser to gain traction in the mobile space without it.

I'm pretty sure that the TC39 consensus would be for [[Prototype]] to be immutable, if we didn't have the __proto__ legacy issue.