6

Question:

As soon as I add the below code to my html page, I get:

Line: 4
Error: Object doesn't support the property or method "exec".

This is the prototype that causes the bug:

    Object.prototype.allKeys = function () {
        var keys = [];
        for (var key in this) 
        {
            // Very important to check for dictionary.hasOwnProperty(key) 
            // otherwise you may end up with methods from the prototype chain.. 
            if (this.hasOwnProperty(key)) 
            {
                keys.push(key);
                //alert(key);
            } // End if (dict.hasOwnProperty(key)) 

        } // Next key

        keys.sort();
        return keys;
    }; // End Extension Function allKeys

And this is the minimum code required to reproduce the error (Browser in question: IE9):

<!DOCTYPE html>
<html>
<head>
    <title>TestPage</title>

    <script type="text/javascript" src="jquery-1.9.1.min.js"></script>


    <script type="text/javascript">
        /*
        Object.prototype.getName111 = function () {
            var funcNameRegex = /function (.{1,})\(/;
            var results = (funcNameRegex).exec((this).constructor.toString());
            return (results && results.length > 1) ? results[1] : "";
        }; // End Function getName
        */

        Object.prototype.allKeys = function () {
            var keys = [];
            for (var key in this) 
            {
                // Very important to check for dictionary.hasOwnProperty(key) 
                // otherwise you may end up with methods from the prototype chain.. 
                if (this.hasOwnProperty(key)) 
                {
                    keys.push(key);
                    //alert(key);
                } // End if (dict.hasOwnProperty(key)) 

            } // Next key

            keys.sort();
            return keys;
        }; // End Extension Function allKeys

    </script>

</head>
<body>

    <select id="selLayers" name="myddl">
      <option value="1">One</option>
      <option value="2">Twooo</option>
      <option value="3">Three</option>
      <option value="4">Text1</option>
      <option value="5">Text2</option>
    </select>


    <script type="text/javascript">

        //var dict = { "de": { "Text1": "Ersetzung 1", "Text2": "Ersetzung 2" }, "fr": { "Text1": "Replacement 1", "Text2": "Réplacement 2" }, "it": { "Text1": "Replacemente 1", "Text2": "Replacemente 2" }, "en": { "Text1": "Replacement 1", "Text2": "Replacement 2"} };
        /*
        var languages = dict.allKeys();


        for (var j = 0; j < languages.length; ++j) 
        {
            var strCurrentLanguage = languages[j];
            var dictReplacements = dict[strCurrentLanguage]
            var keys = dictReplacements.allKeys();

            //alert(JSON.stringify(dictReplacements));
            //alert(JSON.stringify(keys));


            for (var i = 0; i < keys.length; ++i) {
                var strKey = keys[i];
                var strReplacement = dictReplacements[strKey];

                alert(strKey + " ==> " + strReplacement);
                //alert('#selLayers option:contains("' + strKey + '")');
                //$('#selLayers option:contains("' + strKey + '")').html(strReplacement);
                //$('#selLayers option:contains("Text1")').html("foobar");


            }    
        }
        */


        $('#selLayers option:contains("Twooo")').text('Fish');


        //alert(dict.allKeys());        
        //alert(dict["de"]["abc"]);




        /*

        $('#selLayers option[value=2]').text('Fish');
        $('#selLayers option:contains("Twooo")').text('Fish');
        $('#selLayers option:contains("Twooo")').html('&Eacute;tage');
        // http://stackoverflow.com/questions/7344220/jquery-selector-contains-to-equals

        $("#list option[value=2]").text();

        $("#list option:selected").each(function () {
            alert($(this).text());
        });  

        $("#list").change(function() {
            alert($(this).find("option:selected").text()+' clicked!');
        });

        */

    </script>

</body>
</html>

I tried renaming the prototype function, just in case it conflicts with any jquery prototype, but that doesn't help at all.

Pointy
  • 405,095
  • 59
  • 585
  • 614
Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442

3 Answers3

6

Because this is going to add an enumerable item to every single object. Sizzle (which jQuery uses) uses object literals to configure their selector parsing. When it loops these config objects to get all tokens, it doesn't expect your function. In this case, it's probably trying to use your function as a RegExp.

Imagine this scenario:

var obj = { a: 1, b: 2, c: 3 };
var result = 0;
for (var prop in obj) {
  // On one of these iterations, `prop` will be "allKeys".
  // In this case, `obj[prop]` will be a function instead of a number.
  result += obj[prop] * 2;
}
console.log(result);

If you have added anything to Object's prototype that can't be used as a number, you will get NaN for your result.

A good solution to this problem is to add the allKeys function to Object instead of Object.prototype. This mimics Object.keys:

Object.allKeys = function (obj) {
    var keys = [];
    for (var key in obj) 
    {
        // Very important to check for dictionary.hasOwnProperty(key) 
        // otherwise you may end up with methods from the prototype chain.. 
        if (obj.hasOwnProperty(key)) 
        {
            keys.push(key);
            //alert(key);
        } // End if (dict.hasOwnProperty(key)) 

    } // Next key

    keys.sort();
    return keys;
}; // End Extension Function allKeys
benekastah
  • 5,651
  • 1
  • 35
  • 50
  • 1
    I'm going to to with [the system](http://stackoverflow.com/users/1917390/the-system) on this one. It is a bad idea to add properties to any native prototype. jQuery isn't breaking prototype functions as much as javascript's implementation of `for...in` loops is goofy. – benekastah Feb 18 '13 at 17:31
  • 1
    Yep, for in loops are goofy, I realized that when I wrote this code. That's incidentially why I came up with the allKeys method. Well yea, if others simply don't check for hasOwnProperty, then it truly isn't a good idea. I'm giving you an upvote and accept the answer :) – Stefan Steiger Feb 19 '13 at 07:00
3

Because jQuery doesn't bog down its code with checks for .hasOwnProperty() when enumerating objects.

To do so, is to place guards against bad coding practices. Rather than weigh down their code to accommodate such practices, they require that their users adhere to good practices, like never putting enumerable properties on Object.prototype.

In other words... Don't add enumerable properties to Object.prototype unless you want all your code to run guards against those properties, and you never want to enumerate inherited properties.


FWIW, if you really want to call methods on plain objects, just make a constructor so that you have an intermediate prototype object that can be safely extended.

function O(o) {
    if (!(this instanceof O))
        return new O(o)
    for (var p in o)
        this[p] = o[p]
}

O.prototype.allKeys = function() {
    // ...
};

Now you create your objects like this:

var foo = O({
    foo: "foo",
    bar: "bar", 
    baz: "baz"
});

...and the Object.prototype remains untouched, so plain objects are still safe. You'll just need to use the .hasOwnProperty() guard when enumerating your O objects.

for (var p in foo) {
    if (foo.hasOwnProperty(p)) {
        // do stuff
    }
}

And with respect to JSON data being parsed, you can use a reviver function to swap out the plain objects with your O object.

var parsed = JSON.parse(jsondata, function(k, v) {
    if (v && typeof v === "object" && !(v instanceof Array)) {
        return O(v)
    }
    return v
});
the system
  • 9,244
  • 40
  • 46
  • Fine, but the type of my enumerable associative array produced via JSON seems to be object. – Stefan Steiger Feb 18 '13 at 17:16
  • @Quandary: I'm not sure what you mean. When you cause all objects in the environment to inherit a property *(which is what happens when you extend `Object.prototype`)*, then all iterations of inherited properties will bump into it. – the system Feb 18 '13 at 17:19
  • Yes, that's the sense of prototype. But what you are essentially saying is, that I have to write a static method, pass object as variable to it, and therby adopt bad practices, just because jQuery uses bad coding practices, and not the other way round. – Stefan Steiger Feb 18 '13 at 17:26
  • 1
    @Quandary: How on earth is passing an object to a function a bad practice? And how would you consider it a good thing to expect all code to *be required* to test every property of every enumeration of every object to see if the property is directly on the object? And how is it a good thing to adopt practices that *effectively eliminate* the possibility of inherited enumeration, which can be very useful? All of these things are remedied by adopting good practices, like not causing all objects in an environment to inherit an enumerable property. – the system Feb 18 '13 at 17:36
3

You may be able to overcome this side-effect by using defineProperty which allows for setting descriptors.

Object.defineProperty(Object.prototype, 'propName', {value: 'your value', enumerable: false});
marekful
  • 14,986
  • 6
  • 37
  • 59
  • True. Introduced in JavaScript 1.8.5. – marekful Feb 18 '13 at 17:23
  • +1 - Nice find. In my case however, if I was using ONLY newer browsers, I could use the object.keys function anyway. But I ain't ONLY using newer browsers [not that I wouldn't like to] :) – Stefan Steiger Feb 19 '13 at 07:58