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

Friday, April 11, 2014

All IE Objects Are Kinda Broken

Update III
Microsoft landed a patch to supported browsers, including IE9 up to IE11 Desktop.
It's not clear what's going on with Windows Phone IE, but I'd expect at least version 11 to be patched since AFAIK Surface devices have been already fixed as promised before.
I personally strongly doubt IE9 Mobile in WP7 will be patched too but finger crossed for at least WP8 hoping with the 8.1 update this problem will dissolve as well as on Desktop!
Update II there is a better solution
Update
I had a chance to talk with @jdalton and even before that, @bterlson confirmed me they are already working on a fix for the next version of IE and considering patching previous versions: I've also added a third alternative solution to the problem itself, suggested by the same John David Dalton ... I've talked about this option later on so you can check what is this about.
I've posted out of surprise and gut because I really appreciate all progress IE is doing since version 9 so let's hope this is some sort of balsamic vinegar stain that it'll be washed away soon in as many IE versions as possible.
Now, down to the bug ...
Thanks to @bga_ that forwarded to me the following tweet, things are even worst than expected (but keep reading for possible solutions):

Bug Deatils

All IEs that support Object.create seem to be affected. If there are only numeric properties in the created object and no new keyword has been used to inizialize the same, hasOwnProperty check as well as isPropertyEnumerable will miserably fail.
// a generic "class"
function OhComeOn() {}

var a = Object.create(OhComeOn.prototype);
a[1] = 1;
a.hasOwnProperty(1);       // false
a.propertyIsEnumerable(1); // false
for (var k in a) {
  // EVEN IF APPARENTLY NOT ENUMERABLE
  console.log(k); // will be the string "1"
}

var b = new OhComeOn;
b[1] = 1;
b.hasOwnProperty(1);       // true
b.propertyIsEnumerable(1); // true
for (var k in b) {
  console.log(k); // will be the string "1"
}

It does not matter if I use a number or a string as property name or value, it's exactly the same. The only way to make IE believe there is some numeric property to consider too is the following one:
var a = Object.create(OhComeOn.prototype);
a[1] = 1;

// could be anything that is not numeric
a._ = 1;

a.hasOwnProperty(1);       // true
a.propertyIsEnumerable(1); // true
for (var k in a) {
  console.log(k); // will be the string "1", then "_"
}

Fixed If A Descriptor Is Used Instead

This is where the fun begins, as soon as a descriptor is used, everything is awesome.
var a = Object.create(
  OhComeOn.prototype,
  {1:{enumerable:true}}
);

a.hasOwnProperty(1);       // true
a.propertyIsEnumerable(1); // true
Even if not specified as propertyIsEnumerable, the hasOwnProperty check will be trustable once descriptors are used instead of direct access.
var a = Object.create(
  OhComeOn.prototype
);

Object.defineProperty(a, 1, {});

a.hasOwnProperty(1);       // true
a.propertyIsEnumerable(1); // false

Fixing Only These Cases

Accordingly with above behavior, we can say that if there is such bug, and no descriptor is used, most likely we are safe simply polyfilling the behavior:
// note: NOT suitable (yet!) for dictionaries!
var create = Object.create;
if (!function(o){
  o[1] = 1;
  return o.hasOwnProperty(1);
}(create({}))) {
  create = (function(create){
    function Class() {
      // never forget to free the
      // reference counter
      // to the external proto!
      Class.prototype = null;
    }
    return function (p, d) {
      return d ?
        create(p, d) :
        new Class(Class.prototype = p)
    };
  }(create));
}
With above function, previous examples should work as expected:
var a = create(
  OhComeOn.prototype
);

a[1] = 1;

a.hasOwnProperty(1);       // true
a.propertyIsEnumerable(1); // true
Unfortunately, this is not the only problem we gonna have with such bug ...

Corrupted And Doomed Dictionaries

What we cannot do is to use above snippet to create dictionaries, since this does not work as we expect:
function Dictionary() {}
// this is not the equivalent of null objects
Dictionary.prototype = null;


// here the proof
var d = new Dictionary;
d.toString; // function {} [native code]

d instanceof Object // true indeed

Setting null as prototype value never worked like that so we need to find a solution ... but how ...
  1. adding and deleting a non numeric property won't work (with latest updated we'll see it works with numeric properties though as hack to fix the problem)
  2. using Object.defineProperty per each property would be boring, verbose, and slow
  3. descriptors, if empty, do not solve the problem
There must be a non numeric property in order to have reliable dictionaries, otherwise these won't be reliable as dictionaries using the most common check we all know since ever: hasOwnProperty.

First Possible Work Around: The in Operator

Take that JSLint, the most secure way to have dictionaries that behaves as dictionaries is to use the in operator that does not suffer from this bug.
var n = Object.create(null);

n[1] = 1;

if (1 in n) {
  alert('hooraaaay');
}
For dictionaries only, in seems to be the preferred choice also because these properties will show up in for/in loops, but this will break like a charm:
var n = Object.create(null);

n[1] = 1;

if (var k in n) {
  // the infamous check
  // that will FAIL in IE
  if (Object.prototype.hasOwnProperty.call(n, k)) {
    alert(k); // will never happen!
  }
}
The problem here is that most developers are still not ready to abandon JSLint ES3 era hints and embrace dictionaries in a way that is hybrid with these platform, so here I am with the only possible extra solution.

Second Work Around: The Falsy Property

There is no way we deal on daily basis with empty strings as property, unless we are implementing some other sort of hack. I am talking about a solution based on a property that will NOT enumerate, but will FIX properties check.
For unobtrusivity sake, this property should be also configurable and writable.
var n = Object.create(null, {'':{
  configurable: true,
  writable: true
}});

n[1] = 1;

{}.hasOwnProperty.call(n, 1);       // true
{}.propertyIsEnumerable.call(n, 1); // true

for (var k in n) {
  console.log(k); // will be **only** '1'
  // since '' is not enumerable
}

Above solution seems to better fit in current JS world where properties are usually filtered as if (key) {obj[key] = value} plus all own properties will be most likely checked over for/in loops.
More On The Choice Of An Empty String
If you are wondering what's special about the empty string here the answer: when you use JSON.parse(str, revivalCallback) the empty string or the empty key means that the JSON object is fully ready and that would be the last iteration. Since empty property has such strong meaning for JSON, I believe nobody really use empty strings anywhere and for no reason in any library. I also believe nobody ever wrote an empty property name in JSON but in any case this is not a problem because the fix I've proposed will never show up in JSON.parse since it's both not enumerable plus JSON has nothing to do with Object.create so it's the safest property I could think about.

Update II - Actually Not Needed!

Thanks again to Dmitry Korobkin and his further researches: We have a proper solution that won't require any empty string hack.
TL;DR if we set a configurable number and we delete it the object will behave as expected later on ... so ...

Wrapping Up A Solution

In order to have above behavior and a solution for all normal objects in place, here the modified version of initial script:
var create = Object.create;
if (!function(o){
  o[1] = 1;
  return o.hasOwnProperty(1);
}(create({}))) {
  create = (function(create, configurable, define){
    return function (p, d) {
      var r = d ? create(p, d) : create(p);
      // if already own property, nothing to do
      return configurable.hasOwnProperty.call(r, 0) ?
        r : (
        // adding and deleting the numeric property
        delete define(r, 0, configurable)[0],
        // so that now the object is "fixed"
        r
      )
    };
  }(
    create,
    {configurable:true},
    Object.defineProperty
  ));
}
Above snippet is the less obtrusive when it comes to Object.getOwnPropertyNames since the empty property will never show up, no matter if it's a dictionary or not.
While this is in my opinion the best fix for the problem, there might some performance implication since the internal class of each object won't be shared anymore and this can cause de-optimizations per each created object.
Putting together a solution that fixes the problem and is not obtrusive is AFAIK not possible so here yet another snippet based on both new chain and the empty property.
var create = Object.create;
if (!function(o){
  o[1] = 1;
  return o.hasOwnProperty(1);
}(create({}))) {
  create = (function(create, empty){
    function Class() {
      Class.prototype = null;
    }
    return function (p, d) {
      // if p is null
      // or there is a descriptor
      return p === null || d ?
        // crete via descriptor or empty
        create(p, d || empty) :
        // chain via good old prototype
        new Class(Class.prototype = p)
      ;
    };
  }(create, {'':{
    enumerable:   false,
    writable:     true,
    configurable: true
  }}));
}
The main particular caveat, a part from the empty property that will show up in Object.getOwnPropertyNames when it comes to Dictionaries, although hopefully we don't need that so much over Dictionaries like instances, is that passing an empty object as descriptor might confuse the initialization so ... just, don't.
var obj = create(
  Array.prototype,
  {} // <==== DON'T DO THIS!
);
If we want to be extra sure that with Dictionaries the Object.getOwnPropertyNames won't expose our initial hack, we can tweak that too via:
Object.defineProperty(
  Object,
  'getOwnPropertyNames',
  function(original){
    var gOPN = original.value;
    function notTheEmptyOne(k) {
      return !!k;
    }
    original.value = function (o) {
      return gOPN(o).filter(notTheEmptyOne);
    };
    return original;
  }(
    Object.getOwnPropertyDescriptor(
      Object,
      'getOwnPropertyNames'
    )
  )
);
Above hack might be improved accordingly, but that's just a hint I believe not so necessary anyway.

The John-David's Alternative: Don't Object.create(null)

Since using a Dictionary is somehow a doomed choice due some V8 and old WebKit __proto__ implementation, it might make sense to use something like Set/Map from ES6 or a simple HashMap like object instead:
function HashMap() {
  // (C) Andrea Giammarchi - Mit Style
  Object.defineProperty(this, '_', {value:
    Object.create(null)
  });
}
Object.defineProperties(
  HashMap.prototype,
  {
    // clear all the values
    // returns the HashMap
    clear: {value: function () {
      var keys = this.keys(),
          i = keys.length;
      while (i--) delete this._['@' + keys[i]];
      return this;
    }},
    // delete a key, returns true/false
    // accordingly if successful
    del: {value: function (key) {
      return delete this._['@' + key];
    }},
    // get the value or undefined
    get: {value: function (key) {
      return this._['@' + key];
    }},
    // returns true if present, false otherwise
    has: {value: function (key) {
      return ('@' + key) in this._;
    }},
    // all stored keys
    keys: {value: function () {
      var r = [], k;
      for (k in this._) r.push(k.slice(1));
      return r;
    }},
    // set a value and returns it
    set: {value: function (key, value) {
      return this._['@' + key] = value;
    }}
  }
);
With above like utility we can have a universally safe ES5 Dictionary that will never perform like an Object.create(null) but will surely work as expected.

Who Fixed The Initial Problem Already?

Not so many libraries I know have a fix for such issue, at least jsCore and my latest prototypal one has been updated using the empty string solution for dictionaries.
The purpose of prototypal is indeed to bring inheritance in every browser, included those not even compatible with Object.create so, if the test is green, you are safe from problems and ready to go.

WTF IE !

Well, I have no other words to describe this absurd bug with plain JavaScript objects ... all I know is that old IE always had problems with plain objects too, i.e.
// IE < 9 [[DontEnum]] bug
for (var key in {toString:1}) {
  alert('old IE has never been here');
}

Today we also have this Object.create(proto) bug which is a very problematic one and specially for dictionaries, where knowing that some number has been set as own property to a null object might not even be such an edge case, surely less edge ... although it took years to realize there is such problem!

Please Fix it ASAP IE/Chakra Team, thank You!

1 comment:

MarkM said...

https://codereview.appspot.com/91140043/