JavaScript: Muddy Waters of Reflection 2

Just like a lot of other people I have a love-hate relationship with JavaScript. I love how expressive it is and I love that it runs everywhere but there are also some things where the languages falls very short.

The gripe of the day is how JS makes reflection part of the everyday programming flow. And it is not reliable reflection, it is crippled reflection with lots of gotchas. I’ll focus on two reflection mechanisms in JS today, for-in loops and square bracket annotations (member lookup expression, array['length']). The foor-in loop is used to loop over the properties of an object. But it only includes enumerable properties. Also, it incudes the enumerable properties of the objects prototype. The member lookup expression is used to lookup a property based on its name, which is always a string.

The problem is that people tend to use this reflective mechanism to do Map data structures. It is not surprising since there is no Map data structure in JS (before ES6) and the syntax is very convenient. Of course you want to write what you mean; map[key] = value instead of map.set(key, value).

Lets go on. What if I want to add a size getter on my map that returns the number of entries in the map? Now I can no longer add an entry with the key "size" since that would replace or shadow the size getter. This is a real problem and a lot of web site crash if you enter the string "__proto__" or the string "hasOwnProperty. For example Google Docs used to crash if you entered the former of those two.

What if I wanted to have non string keys? Nope, only strings are allowed and the key will be the toString() string of the key object. The common solution to this is to build a Map data structure where the key is a unique ID (UID) and the UID gets added to the real key object. This of course breaks if the key is a non extensible objects and those of you who have been around for a long time might remember that Nodes coming from an XHR in IE where non extensible causing all kinds of head aches.

The Future

ES6 has a new kind of loop called for-of. The for of loop is not a reflective loop. It is an iteration loop. The object that you want to iterate over must have an iterator and by default Object.prototype does not. Array.prototype on the other hand has an iterator that iterates over the elements in the array. Just what you always wanted!

for (let value of [0, 'one', true]) {
  console.log(value);
}

for (let value of {a: 1}) {  // throws not iterable
  console.log(value);
}

ES6 has Map and Set which allows any value as a key. This solves some of the issues but we still have the mess with the member lookup expression.

There is a proposal on the ES wiki that tries to reform the object model and decouple [] from property lookup. The basic idea is that any object can override the [] operations. Existing objects would continue to work as they do today but new objects could opt in to this new behavior.

For example, we could extend the ES6 Map objects to allow [] and []= like this:

import {elementGet, elementSet} from '@name';
Map.prototype[elementSet] = function(key, value) {
  return this.set(key, value);
};
Map.prototype[elementGet] = function(key) {
  return this.get(key);
};

var m = new Map;
var key = {};
m[key] = 42;
console.log(m[key]);

The reformed object model also allows us to finally explain how arrays work. It also allows us to implement all of the DOM interfaces in pure JavaScript. For example we could implement NodeList like this:

import {create, elementGet} from '@name';
const ownerName = create();
const lengthName = create();

export function NodeList(owner) {
  this[ownerName] = owner;
}

NodeList.prototype  = {
  get length() {
    let i = 0;
    for (let node = this[ownerName].firstChild; node; node = node.nextSibling) {
      i++;
    }
    return i;
  },

  [elementGet]: function(index) {
    let i = 0;
    for (let node = this[ownerName].firstChild; node; node = node.nextSibling) {
      if (i++ === index)
        return node;
    }
    return node;
  }
};

Today

Map and Set are implemented in SpiderMonkey and V8 and is available under a flag in Chrome and in Firefox without a flag.

For-of loops are available in Firefox. V8 hasn’t started working on for-of loops nor iterators so don’t expect to be able to use these without a transpiler any time soon.

Both for-of loops and the reformed object model are available in the Traceur compiler. There is a naïve demo of a string keyed map too. Note that [] keys are not yet supported in object/class literals so the demo uses assignment instead.

2 thoughts on “JavaScript: Muddy Waters of Reflection

  1. Reply lm Mar 2, 2013 03:44

    Great, however this doesn’t solve serialization problem. I don’t care about Map object if it cannot be represented in JSON.

    In ideal world [] operator would access only own properties, and only the dot operator would search prototype chain. Also Object would have no standard members (all would be part of Object.prototype).

  2. Reply Erik Arvidsson Mar 10, 2013 12:25

    Im: There are plenty of ways one can encode a Map in JSON. Here is one:

    Map.prototype.toJSON = function() {
    return [for (entry of this) entry];
    };

    > m = new Map;
    > m.set(1, true);
    > m.set(false, ‘f’);
    > m.set(‘b’, {});
    > JSON.stringify(m)
    // [[1, true], [false, "f"], ["b", {}]]

    Even, if you limit [] to own properties it would shadow methods and break the size usecase.

    I don’t understand the idea that Object would have no standard members? Are you referring to Object.keys etc?

Leave a Reply