I Object

By Douglas CrockfordMay 10th, 2007

One of the two really clever ideas in JavaScript is that objects are dynamic collections with differential inheritance. Differential inheritance means that when object B inherits from object A, object B does not have to contain a copy of all of object A’s stuff. Object B only needs to contain the differences. This saves time and memory, and it allows the augmenting of an object system after the objects have been instantiated. That can be really useful in Ajax applications which have to deal with variable network latency.

Unfortunately, JavaScript got the details wrong. As we saw in for in Intrigue, the for statement can interact badly with methods supplied by the prototype. A little bit of care is required to make for statements work correctly.

Similar care is also required when using objects as general collections. Since methods act like any other member, you can sometimes get false positives from inherited methods.

For example, let’s say we have an application which will look at the words in a text, and that we will take some sort of action if a word matches a word on our list. Using the string split method, we can easily transform a text into an array of words. And we can use an object to make our list of special words.

var myList = {
hope: true,
springs: true,
eternal: true
};

We then loop over the array of words, and on each iteration we will match a word against the list.

if (myList[arrayOfWords[i]]) {
// A match!
}

So we won’t match "construct". But we will match "hope". Great. Our code works. Or does it? This code will also match "constructor" even though "constructor" is not in our list. Why is this? It is because every object has "construct" member. So we get a false positive. Any member anywhere in the prototype chain can cause a false positive.

So how can we fix this? JavaScript’s in operator is useless because it produces the same false positive. We could take advantage of the fact that all of the values in myList are true by doing exact comparisons.

if (myList[arrayOfWords[i]] === true) {
// A match!
}

That works. But suppose we want myList‘s values to be functions which will be called when a key matches. In that case, we can’t look for a known value, and

if (typeof myList[arrayOfWords[i]] === 'function') {
// A match!
}

won’t work because myList.constructor is also a function. What we can do is explicitly reject keys that come from the prototype chain.

var word = arrayOfWords[i];
if (myList.hasOwnProperty(word) && myList[word]) {
// A match!
}

So the solution here is the same solution required to make for statements work correctly: the hasOwnProperty method. If we use it to guard against unintended inheritance, then we can safely use JavaScript’s objects as general containers. We can make JavaScript work correctly at the cost of a bit of ugliness in our code.

It is a nuisance, it is irritating, and it feels like a waste of time. You could also say the same thing about washing your hands after using the restroom. Do it anyway. Good hygiene is good for you. And your code.

12 Comments

  1. Excellent article. Could you also roll that code up into prototype method called “properties” for your objects that returned a list of members specific to that object and make your code a little cleaner?

  2. A trick I use to detect what methods have been added to Array and Object, is to include this script right after my libraries. Then any time I use the for/in I ignore these keywords.

    var keysToIgnore = function() {
    var keys = [];
    var arr = [], obj = {};
    for (var k in arr) {keys.push(k);}
    for (var k in obj) {keys.push(k);}
    return keys;
    }();

  3. So, seems that using members for a purpose never intended (and arguably in a way many developers would initially find confusing) gets you into trouble. No surprise there. Is it the language’s fault … or the coders?

  4. @Matt : it doesn’t work. For example, if keys are added to the Objet constructor after the computation of “keysToIgnore”.

    “hasOwnProperty” is not supported on IE 5.

  5. It’s always refreshing to work with a language that constantly forces you to examine it’s warts in order for you work with it
    correctly. :)

    It should be noted that Prototype’s Ruby-inspired collection facilities (Enumerable, Array, and Hash) allow you to abstract this fact-checking to a single place. Of course, we will always have to pick our flavor of pain: the “break” functionality in Prototype is accomplished with a global hack. Additionally, the current uncompressed version of Prototype is now weighing in at ~ 95kb.

    Ahh, sweet sweet Javascript – you are a harsh mistress.


    /* NOTE - console.log requires Firebug: http://www.getfirebug.com/ */

    /* spits out all links' hrefs and all the wonderful native goodness */
    console.log("Native");
    var links = document.getElementsByTagName('a');
    for(var link in links){
    console.log(link + ":" + links[link]);
    }

    console.log(" ");

    /* spits out all links' hrefs */
    /* REQUIRES PROTOTYPE */
    console.log("Massaged");
    links = $A(document.getElementsByTagName('a'));
    links.each(function(link){
    console.log(link.href);
    });

  6. I wasn’t really surprised when you started using hasOwnProperty() in json.js ;)

    Although some people still consider prototyping Object ‘verboten’ I do think there are some valid reasons to do so, like when you want to backport some general object methods to older browsers.

    toJSONString() is a good example because it will probably become a standard method in future javascript, so the only way to have the same support in older (now current) browsers will be to prototype this method on Object.

    I once wrote a blogpost about that which also contains a nice snippet for browsers that do not natively support hasOwnProperty() and which ironically and necessarily uses Object.prototype: .toJSONString() and Object.prototype

  7. hasOwnProperty isn’t quite bulletproof, because on some browsers myList.hasOwnProperty(“__proto__”) will return true – since all objects have a __proto__ member pointing to their prototype. And then, if you then make any changes to myList["__proto__"], you’ll be messing with the object prototype. Nice.

    Of course, “__proto__” doesn’t turn up that often. But this can lead to odd and subtle bugs.

  8. I agree with Daniel. The use of a raw abject as a general hash map in JavaScript can not be totally bulletproof.

  9. Daniel brings up an interesting case. __proto__ is a non-standard deviation from the ECMAScript standard that can be found in the Mozilla browsers. It can cause good programs to fail.

  10. If you use a JavaScript object as a general hash map (i.e. with undetermined keys), it is even easier to make your program fail:

    var obj = {};

    obj["hasOwnProperty"] = “coconut”;

    alert(obj.hasOwnProperty(“a”));

  11. hasOwnProperty can be avoided by using the following:

    if(myList[word] && (myList[word] !== {}[word]){

    }

    This would also allow for the myList object to inherit non-owned properties from the prototype chain.