Extending JavaScript – The Right Way

Extending JavaScript - the Right WayJavaScript comes with a lot of great functionality built in, but what if there is a function you need which is missing. How can we build them in seamlessly in an elegant way that extends the functionality of our beloved JavaScript. The following will outline a couple methods to extend the existing functionality of JavaScript, both effective but one a little more functionally complete.

Say we want to create a function .capitalize() to extend the String type of JavaScript so that it behaves in a way similar to String.toLowerCase() or String.toUpperCase(). We can do this by prototyping the String object adding in our new function.

This is usually done in the simplest way with the code below:
if(!String.prototype.capitalize)
{
    String.prototype.capitalize = function()
    {
        return this.slice(0,1).toUpperCase() + this.slice(1).toLowerCase();
    }
}
This works fine and dandy but lets say we were to do the following:
var strings = "yay";
for(i in strings) console.log(i + ":" + strings[i]);
We would get the output:
0: y
1: a
2: y
capitalize: function () { return this.slice(0, 1).toUpperCase() + this.slice(1).toLowerCase(); }
Our capitalize function shows up in our for loop, which is correct since it is now a property of all strings. The reason it is doing this is because the enumerable property for our new function is by default set to true.

However, this was wreaking havoc on a plugin I had written that happened to be iterating through each character in a string. By simply changing the enumerable property to false we can avoid this problem which can be done by using the defineProperty method like so:
if(!String.prototype.capitalize)
{
    Object.defineProperty(String.prototype, 'capitalize',
    {
       value: function()
       {
           return this.slice(0,1).toUpperCase() + this.slice(1).toLowerCase();
       },
       enumerable: false
    });
}
Now when we run our loop we get the outcome we were more likely expecting.
var strings = "yay";
for(i in strings) console.log(i + ":" + strings[i]);
0: y
1: a
2: y
The key thing to note is that just because we can’t see it in the for loop, doesn’t mean it’s not there, just like any other property, we can call it, change it or delete it.
var strings = "yay";
console.log(strings.capitalize)
function () { return this.slice(0, 1).toUpperCase() + this.slice(1).toLowerCase(); }
The reason for this may not seem as obvious when we’re looking at strings, but it gives us a lot of flexibility should we need it. It can come in really handy when defining our own objects and setting some default values that we would want to expose.

Below are just a few more examples you may wish to use in some of your own projects:

  • String.pxToInt()
    Convert css px value like “200px” to an Integer.
    if(!String.prototype.pxToInt)
    {
        Object.defineProperty(String.prototype, 'pxToInt',
        {
            value: function()
            {
                return parseInt(this.split('px')[0]);
            },
            enumerable: false
        });
    }
    
  • String.isHex()
    Checks if string is valid Hex value such as #CCC or #CACACA.
    if(!String.prototype.isHex)
    {
        Object.defineProperty(String.prototype, 'isHex',
        {
            value: function()
            {
                return this.substring(0,1) == '#' &&  
                       (this.length == 4 || this.length == 7) && 
                       /^[0-9a-fA-F]+$/.test(this.slice(1));
            },
            enumerable: false
        });
    }
    
  • String.reverse()
    Reverse a string.
    if(!String.prototype.reverse)
    {
        Object.defineProperty(String.prototype, 'reverse',
        {
            value: function()
            {
                return this.split( '' ).reverse().join( '' );
            },
            enumerable: false
        });
    }
    
  • String.wordCount()
    Count the number of words in a given string, words being separated by spaces.
    if(!String.prototype.wordCount)
    {
        Object.defineProperty(String.prototype, 'wordCount',
        {
            value: function()
            {
                return this.split(' ').length;
            },
            enumerable: false
        });
    }
    
  • String.htmlEntities()
    Converts HTML characters like < and > to HTML encoded special characters.
    if(!String.prototype.htmlEntities)
    {
        Object.defineProperty(String.prototype, 'htmlEntities',
        {
            value: function()
            {
                return String(this).replace(/&amp;/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
            },
            enumerable: false
        });
    }
    
  • String.stripTags()
    Strips out all HTML tags from the string.
    if(!String.prototype.stripTags)
    {
        Object.defineProperty(String.prototype, 'stripTags',
        {
            value: function()
            {
                return this.replace(/<\/?[^>]+>/gi, '');
            },
            enumerable: false
        });
    }
    
  • String.trim()
    Removes all leading and trailing white space from string.
    if(!String.prototype.trim)
    {
        Object.defineProperty(String.prototype, 'trim',
        {
            value: function()
            {
                return this.replace(/^\s*/, "").replace(/\s*$/, "");
            },
            enumerable: false
        });
    }
    
  • String.stripNonAlpha()
    Removes all non-alphanumeric characters from string.
    if(!String.prototype.stripNonAlpha)
    {
        Object.defineProperty(String.prototype, 'stripNonAlpha',
        {
            value: function()
            {
                return this.replace(/[^A-Za-z ]+/g, "");
            },
            enumerable: false
        });
    }
    
  • Object.sizeof()
    The the size of an object, for example: {one: “and”, two: “and”} would equal 2
    if(!Object.prototype.sizeof)
    {
        Object.defineProperty(Object.prototype, 'sizeof',
        {
            value: function()
            {
                var counter = 0;
                for(index in this) counter++;
                
                return counter;
            },
            enumerable: false
        });
    }
    
Duck Punching jQuery Ajax
JavaScript RGB to Hex Converter
12 Awesome jQuery Selector Extensions
jQuery Plugin Development Boilerplate
10 Coding Tips to Write Superior jQuery Plugins

This entry was posted in JavaScript. Bookmark the permalink.

18 comments on “Extending JavaScript – The Right Way

  1. Roatin Marth on said:

    The most cross-engine way of iterating a string is to use a standard for loop (i=0 to string.length) and string.charAt(i) inside the loop. It has worked since 1996 and will continue to work! Then you can extend String.prototype all you want, in any engine.

    Extending Object is a different story of course. Your use of Object.defineProperty to define Object.sizeof there is very necessary.

    (side note: Object.sizeof(obj) can be replaced with standard ECMAScript5′s Object.keys(obj).length).

  2. Larry Battle on said:

    Great post. It seems that I need to get use to using Object.defineProperty after reading this post. I’m hesitant to use Object.defineProperty due to lack of browser support but that can easily be solved if I define it when it doesn’t exist.

    underscore.string.js is a library that provides a ton of additional string functionality.
    http://epeli.github.com/underscore.string/lib/underscore.string.js

    Suggestion:

    this.substring(0,1) should be this.charAt(0)

  3. Ilan on said:

    Peace, websanova,

    Just to let you know, WordPress is munging your htmlEntities: you wanted to show code, not the HTML entities themselves. Escape the “&” somehow.
    I forget what I did to get rid of this persistent bug, whether it’s one of the code syntax related plugins, or built-in filter’s (the_content) fault, or TinyMCE (confused when switching between HTML and WYSIWYG views)…?
    ;o)

    • Elsa on said:

      Matt,I haven’t actually ceckehd, but I’d be utterly shocked if Prototype didn’t do this:if (! document.getElementsByClassName) {document.getElementsByClassName = function(…) {…}}It’s about making browsers consistent, not overriding built-in functionality (unless it’s incorrect). So you use the native implementation wherever you can, supply a non-native one wherever it’s needed, and have a consistent interface across browsers.

  4. Andreas Farre on said:

    ES5 already has String.prototype.trim.

  5. Jesse Tuxin on said:

    You should have mentioned that Object.defineProperty is only implemented in ECMAScript5, so you can’t use it with IE < 9. Also you can't delete or change a property by default, for that you'd need to add writable:true and configurable: true to the object you pass into defineProperty. Everything defaults to false. The Object MDN article is a very good reference for anyone wanting to use this method.

  6. Hans PUFAL on said:

    Taking this one step further, this defines a method ‘implement’ on object which simplifies the process:

    !Object.implement && Object.defineProperty (Object.prototype, \'implement\', {
    enumerable: false,
    value: function (name, func) {
    !this.prototype[name] && Object.defineProperty (this.prototype, name, {
    enumerable: false,
    value: func
    });
    }
    });

    String.implement (\'reverse\', function () {
    return this.split (\'\').reverse ().join (\'\');
    });

    String.implement (\'stripAlpha\', function () {
    return this.replace(/[^A-Za-z ]+/g, \'\');
    });

    With the upcoming fat arrow notation it becomes even simpler :

    String.implement (\'reverse\', () => this.split (\'\').reverse ().join (\'\'));

    String.implement (\'stripAlpha\', () => this.replace (/[^A-Za-z ]+/g, \'\'));

    • Hans PUFAL on said:

      An more elegant formulation of Object.implement which gets the method name from the function parameter:

      !Object.implement && Object.defineProperty (Object.prototype, \'implement\', {
      enumerable: false,
      value: function (func) {
      var name = (func.toString().match (/^functions+([a-z$_][a-z0-9$_]+)/i) || [\'\', \'\'])[1];
      name && !this.prototype[name] && Object.defineProperty (this.prototype,
      name, { enumerable: false, value: func });
      }
      });

      // example :
      String.implement (function trim () {
      return this.replace(/^s*/, \"\").replace(/s*$/, \"\");
      });

      • Is Function.name going away in ES5? If not, why wouldn’t you want to just say var name = func.name, instead of trying to figure out the name?

        Plus, toString can be overwritten so it may not even give you the right stuff to match against.

        • Hans PUFAL on said:

          function.name is not ECMASCRIPT, it is a Mozilla extension : https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/Name

          I’d be interested in any ideas on avoiding the dependency on toString.

    • Hans PUFAL on said:

      Version 3. Allows explicit or implicit specification of function name, also the cfg parameter allows the configurable property to be set true, it is false by default.

      !Object.implement && Object.defineProperty (Object.prototype, \'implement\', {
      // based on http://www.websanova.com/tutorials/javascript/extending-javascript-the-right-way
      value: function (mthd, fnc, cfg) { // adds fnc to prototype under name mthd
      if (typeof mthd === \'function\') { // find mthd from function source
      cfg = fnc, fnc = mthd;
      (mthd = (fnc.toString ().match (/^functions+([a-z$_][w$]+)/i) || [0, \'\'])[1]);
      }
      mthd && !this.prototype[mthd] &&
      Object.defineProperty (this.prototype, mthd, {configurable: !!cfg, value: fnc, enumerable: false});
      }
      });

      // demo :

      String.implement (\'stripAlpha\', () => this.replace (/[^A-Za-z ]+/g, \'\'));

      String.implement (function trim () {
      return this.replace(/^s*/, \"\").replace(/s*$/, \"\");
      });

      // Add additional parameter value true to make the method configurable.

      • Roccimm on said:

        I found a slightly simeplr way to do what you want.All:jQuery.grep(images, function(o){return o.status == “ready”;}).length == images.lengthAny:jQuery.grep(images, function(o){return o.status == “ready”;}).length > 0Also, have you looked at the ? I haven’t used it myself, but it might have some of the stuff you’re missing.If I were you I would try and avoid bringing stuff over directly from Prototype. It might stifle your JQuery indoctrination. :-)

    • Putry on said:

      Patrick,Good call on grep(), that definitely makes the oivcetbje more clear.I’ll have to take a deeper look at the Collection plugin, but I really don’t see what’s wrong with modifying the Array prototype. JavaScript (and ECMAScript) are specifically designed so that you can extend functionality that way why not leverage it and make you code simpler?Maybe it’s just the OO guy in me, but it annoys me that jQuery and CF share “separate state and behaviour” paradigm, and while CF does it for backwards compatibility, jQuery doesn’t have an excuse. Why should I have to supply my “something” to some external procedure to add behaviour, why can’t I just ask the “something” directly?CF: arrayLen(myArray); vs. myArray.length;jQuery: jQuery.grep(myArray, function(){}); vs. myArray.grep(function(){});

  7. itoctopus on said:

    I remember extending the String object with functions similar to the ones above many years ago. Even though this is well known and old, it’s still very handy and makes your life much easier.

    Better include the string JS file in a generic library that is included on every page.

  8. Martin on said:

    It’s hard to overstress the importance of setting enumerable property to false when extending the object prototype. For example, you will break jQuery (and many other libraries) if you fail to do so.

  9. ZER0 on said:

    Is there any reason to explicitly set enumerable to false? A property defined using defineProperty method is not enumerable by default: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty

  10. Zohar Arad on said:

    Great post but you failed to mention that the right way to iterate with for-in loops in JS should be used on conjunction with hasOwnProperty which ensures properties of the iterated object indeed belong to it.

    So, effectively you could add properties to the String object using classical prototyping and simply change your for look to ensure iterated properties belong to the right object

  11. pxToInt is not very useful. parseInt by itself does the same thing: parseInt(\"10px\") === 10.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

HTML tags are not allowed.