Extending JavaScript – The Right Way

Extending JavaScript the Right Way

JavaScript 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
    });
}

18 thoughts on “Extending JavaScript – The Right Way

  1. Roatin Marth

    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).

    Reply
  2. Larry Battle

    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)

    Reply
  3. Ilan

    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)

    Reply
    1. Elsa

      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.

      Reply
  4. Jesse Tuxin

    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.

    Reply
  5. Hans PUFAL

    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, ''));

    Reply
    1. Hans PUFAL

      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 (/^function\s+([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*$/, "");
      });

      Reply
      1. HB

        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.

        Reply
    2. Hans PUFAL

      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/blog/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 (/^function\s+([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.

      Reply
      1. Roccimm

        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. :-)

        Reply
    3. Putry

      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(){});

      Reply
  6. itoctopus

    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.

    Reply
  7. Martin

    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.

    Reply
  8. Zohar Arad

    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

    Reply

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>