Augmenting JavaScript Core Objects

Jeff Friesen

JavaScript defines several objects that are part of its core: Array, Boolean, Date, Function, Math, Number, RegExp, and String. Each object extends Object, inheriting and defining its own properties and methods. I’ve occasionally needed to augment these core objects with new properties and methods and have created a library with these enhancements. In this article, I present various enhancements that I’ve introduced to the Array, Boolean, Date, Math, Number, and String objects.

I add new properties directly to the core object. For example, if I needed a Math constant for the square root of 3, I’d specify Math.SQRT3 = 1.732050807;. To add a new method, I first determine whether the method associates with a core object (object method) or with object instances (instance method). If it associates with an object, I add it directly to the object (e.g., Math.factorial = function(n) { ... }). If it associates with object instances, I add it to the object’s prototype (e.g., Number.prototype.abs = function() { ... }).

Methods and Keyword this

Within an object method, this refers to the object itself. Within an instance method, this refers to the object instance. For example, in " remove leading and trailing whitespace ".trim(), this refers to the " remove leading and trailing whitespace " instance of the String object in String‘s trim() method.

Name Collisions

You should be cautious with augmentation because of the possibility for name collisions. For example, suppose a factorial() method whose implementation differs from (and is possibly more performant than) your factorial() method is added to Math in the future. You probably wouldn’t want to clobber the new factorial() method. The solution to this problem is to always test a core object for the existence of a same-named method before adding the method. The following code fragment presents a demonstration:

if (Math.factorial === undefined)
   Math.factorial = function(n)
                    {
                       // implementation
                    }
alert(Math.factorial(6));

Of course, this solution isn’t foolproof. A method could be added whose parameter list differs from your method’s parameter list. To be absolutely sure that you won’t run into any problems, add a unique prefix to your method name. For example, you could specify your reversed Internet domain name. Because my domain name is tutortutor.ca, I would specify Math.ca_tutortutor_factorial. Although this is a cumbersome solution, it should give some peace of mind to those who are worried about name conflicts.

Augmenting Array

The Array object makes it possible to create and manipulate arrays. Two methods that would make this object more useful are equals(), which compares two arrays for equality, and fill(), which initializes each array element to a specified value.

Implementing and Testing equals()

The following code fragment presents the implemention of an equals() method, which shallowly compares two arrays — it doesn’t handle the case of nested arrays:

Array.prototype.equals =
   function(array)
   {
      if (this === array)
         return true;

      if (array === null || array === undefined)
         return false;

      array = [].concat(array); // make sure this is an array

      if (this.length != array.length)
         return false;

      for (var i = 0; i < this.length; ++i) 
         if (this[i] !== array[i]) 
            return false;
      return true;
   };

equals() is called with an array argument. If the current array and array refer to the same array (=== avoids type conversion; the types must be the same to be equal), this method returns true.

equals() next checks array for null or undefined. When either value is passed, this method returns false. Assuming that array contains neither value, equals() ensures that it’s dealing with an array by concatenating array to an empty array.

equals() compares the array lengths, returning false when these lengths differ. It then compares each array element via !== (to avoid type conversion), returning false when there’s a mismatch. At this point, the arrays are considered equal and true returns.

As always, it’s essential to test code. The following test cases exercise the equals() method, testing the various possibilities:

var array = [1, 2];
alert("array.equals(array): " + array.equals(array));

alert("['A', 'B'].equals(null): " + ['A', 'B'].equals(null));
alert("['A', 'B'].equals(undefined): " + ['A', 'B'].equals(undefined));

alert("[1].equals(4.5): " + [1].equals(4.5));

alert("[1].equals([1, 2]): " + [1].equals([1, 2]));

var array1 = [1, 2, 3, 'X', false];
var array2 = [1, 2, 3, 'X', false];
var array3 = [3, 2, 1, 'X', false];
alert("array1.equals(array2): " + array1.equals(array2));
alert("array1.equals(array3): " + array1.equals(array3));

When you run these test cases, you should observe the following output (via alert dialog boxes):

array.equals(array): true
['A', 'B'].equals(null): false
['A', 'B'].equals(undefined): false
[1].equals(4.5): false
[1].equals([1, 2]): false
array1.equals(array2): true
array1.equals(array3): false

Implementing and Testing fill()

The following code fragment presents the implementation of a fill() method, which fills all elements of the array on which this method is called with the same value:

Array.prototype.fill =
   function(item)
   {
      if (item === null || item === undefined)
         throw "illegal argument: " + item;

      var array = this;
      for (var i = 0; i < array.length; i++)
         array[i] = item;
      return array;
   };

fill() is called with an item argument. If null or undefined is passed, this method throws an exception that identifies either value. (You might prefer to fill the array with null or undefined.) Otherwise, it populates the entire array with item and returns the array.

I’ve created the following test cases to test this method:

try
{
   var array = [0];
   array.fill(null);
}
catch (err)
{
   alert("cannot fill array with null");
}

try
{
   var array = [0];
   array.fill(undefined);
}
catch (err)
{
   alert("cannot fill array with undefined");
}

var array = [];
array.length = 10;
array.fill('X');
alert("array = " + array);

alert("[].fill(10) = " + [].fill(10));

When you run these test cases, you should observe the following output:

cannot fill array with null
cannot fill array with undefined
array = X,X,X,X,X,X,X,X,X,X
[].fill(10) = 

Augmenting Boolean

The Boolean object is an object wrapper for Boolean true/false values. I’ve added a parse() method to this object to facilitate parsing strings into true/false values. The following code fragment presents this method:

Boolean.parse =
   function(s)
   {
      if (typeof s != "string" || s == "")
         return false;

      s = s.toLowerCase();
      if (s == "true" || s == "yes")
         return true;
      return false;
   };

This method returns false for any argument that is not a string, for the empty string, and for any value other than "true" (case doesn’t matter) or "yes" (case doesn’t matter). It returns true for these two possibilities.

The following test cases exercise this method:

alert(Boolean.parse(null));
alert(Boolean.parse(undefined));
alert(Boolean.parse(4.5));
alert(Boolean.parse(""));
alert(Boolean.parse("yEs"));
alert(Boolean.parse("TRUE"));
alert(Boolean.parse("no"));
alert(Boolean.parse("false"));

When you run these test cases, you should observe the following output:

false
false
false
false
true
true
false
false

Augmenting Date

The Date object describes a single moment in time based on a time value that’s the number of milliseconds since January 1, 1970 UTC. I’ve added object and instance isLeap() methods to this object that determine if a specific date occurs in a leap year.

Implementing and Testing an isLeap() Object Method

The following code fragment presents the implementation of an isLeap() object method, which determines if its date argument represents a leap year:

Date.isLeap =
   function(date)
   {
      if (Object.prototype.toString.call(date) != '[object Date]')
         throw "illegal argument: " + date;

      var year = date.getFullYear();
      return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);
   };

Instead of using a date instanceof Date expression to determine if the date argument is of type Date, this method employs the more reliable Object.prototype.toString.call(date) != '[object Date]' expression to check the type — date instanceof Date would return false when date originated from another window. When a non-Date argument is detected, an exception is thrown that identifies the argument.

After invoking Date‘s getFullYear() method to extract the four-digit year from the date, isLeap() determines if this year is a leap year or not, returning true for a leap year. A year is a leap year when it’s divisible by 400 or is divisible by 4 but not divisible by 100.

The following test cases exercise this method:

try
{
   alert(Date.isLeap(null));
}
catch (err)
{
   alert("null dates not supported.");
}

try
{
   alert(Date.isLeap(undefined));
}
catch (err)
{
   alert("undefined dates not supported.");
}

try
{
   alert(Date.isLeap("ABC"));
}
catch (err)
{
   alert("String dates not supported.");
}

var date = new Date();
alert(date + (Date.isLeap(date) ? " does " : " doesn't ") +
      "represent a leap year.");

When you run these test cases, you should observe output that’s similar to the following:

null dates not supported.
undefined dates not supported.
String dates not supported.
Wed Oct 23 2013 19:30:24 GMT-0500 (Central Standard Time) doesn't represent a leap year.

Implementing and Testing an isLeap() Instance Method

The following code fragment presents the implemention of an isLeap() instance method, which determines if the current Date instance represents a leap year:

Date.prototype.isLeap = 
   function()
   {
      var year = this.getFullYear();
      return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);
   };

This version of the isLeap() method is similar to its predecessor but doesn’t take a date argument. Instead, it operates on the current Date instance, which is represented by this.

The following test cases exercise this method:

date = new Date(2012, 0, 1);
alert(date + ((date.isLeap()) ? " does " : " doesn't ") + 
      "represent a leap year.");
date = new Date(2013, 0, 1);
alert(date + ((date.isLeap()) ? " does " : " doesn't ") + 
      "represent a leap year.");

When you run these test cases, you should observe output that’s similar to the following:

Sun Jan 01 2012 00:00:00 GMT-0600 (Central Daylight Time) does represent a leap year.
Tue Jan 01 2013 00:00:00 GMT-0600 (Central Daylight Time) doesn't represent a leap year.

Augmenting Math

The Math object declares math-oriented object properties and methods and cannot be instantiated. I’ve added a GOLDEN_RATIO object property and rnd(), toDegrees(), toRadians(), and trunc() object methods to Math.

About the Golden Ratio

The Golden Ratio is a math constant that frequently appears in geometry. Two quantities are in the golden ratio when their ratio equals the ratio of their sum to the larger of the two quantities. In other words, for a greater than b, a/b = (a+b)/a.

Implementing and Testing GOLDEN_RATIO and rnd()

The following code fragment presents the implemention of the GOLDEN_RATIO constant and the rnd()
method:

Math.GOLDEN_RATIO = 1.61803398874;

Math.rnd =
   function(limit)
   {
      if (typeof limit != "number")
         throw "illegal argument: " + limit;
  
      return Math.random() * limit | 0;
   };

After defining the GOLDEN_RATIO object property, this code fragment defines the rnd() object method, which takes a limit argument. This argument must be numeric; if not, an exception is thrown.

Math.random() returns a fractional value from 0.0 through (almost) 1.0. After being multiplied by limit, a fraction remains. This fraction is removed through truncation and truncation is performed by bitwise ORing 0 with the result.

Bitwise OR uses a ToInt32 internal function to convert its numeric operands to 32-bit signed integers. This operation eliminates the fractional part of the number and is more performant than using Math.floor() because a method call isn’t required.

The following test cases exercise these items:

alert("Math.GOLDEN_RATIO: " + Math.GOLDEN_RATIO);

try
{
   alert("Math.rnd(null): " + Math.rnd(null));
}
catch (err)
{
   alert("null value not supported.");
}
alert("Math.rnd(10): " + Math.rnd(10));

When you run these test cases, you should observe output that’s similar to the following:

Math.GOLDEN_RATIO: 1.61803398874
null value not supported.
Math.rnd(10): 7

Implementing and Testing toDegrees(), toRadians(), and trunc()

The following code fragment presents the implementation of the toDegrees(), toRadians(), and trunc() methods:

Math.toDegrees = 
   function(radians)
   {
      if (typeof radians != "number")
         throw "illegal argument: " + radians;

      return radians * (180 / Math.PI);
   };

Math.toRadians = 
   function(degrees)
   {
      if (typeof degrees != "number")
         throw "illegal argument: " + degrees;

      return degrees * (Math.PI / 180);
   };


Math.trunc =
   function(n)
   {
      if (typeof n != "number")
         throw "illegal argument: " + n;
  
      return (n >= 0) ? Math.floor(n) : -Math.floor(-n);
   };

Each method requires a numeric argument and throws an exception when this isn’t the case. The first two methods perform simple conversions to degrees or radians and the third method truncates it argument via Math‘s floor() method.

Why introduce a trunc() method when floor() already performs truncation? When it receives a negative non-integer argument, floor() rounds this number down to the next highest negative integer. For example, floor() converts -4.1 to -5 instead of the more desirable -4.

The following test cases exercise these items:

try
{
   alert("Math.toDegrees(null): " + Math.toDegrees(null));
}
catch (err)
{
   alert("null degrees not supported.");
}
alert("Math.toDegrees(Math.PI): " + Math.toDegrees(Math.PI));

try
{
   alert("Math.toRadians(null): " + Math.toRadians(null));
}
catch (err)
{
   alert("null radians not supported.");
}
alert("Math.toRadians(180): " + Math.toRadians(180));

try
{
   alert("Math.trunc(null): " + Math.trunc(null));
}
catch (err)
{
   alert("null value not supported.");
}
alert("Math.trunc(10.83): " + Math.trunc(10.83));
alert("Math.trunc(-10.83): " + Math.trunc(-10.83));

When you run these test cases, you should observe the following output:

null degrees not supported.
Math.toDegrees(Math.PI): 180
null radians not supported.
Math.toRadians(180): 3.141592653589793
null value not supported.
Math.trunc(10.83): 10
Math.trunc(-10.83): -10

Augmenting Number

The Number object is an object wrapper for 64-bit double precision floating-point numbers. The following code fragment presents the implementation of a trunc() instance method that’s similar to its object method counterpart in the Math object:

Number.prototype.trunc = 
   function()
   {
      var num = this;
      return (num < 0) ? -Math.floor(-num) : Math.floor(num);
   };

The following test cases exercise this method:

alert("(25.6).trunc(): " + (25.6).trunc());
alert("(-25.6).trunc(): " + (-25.6).trunc());
alert("10..trunc(): " + 10..trunc());

The two dots in 10..trunc() prevent the JavaScript parser from assuming that trunc is the fractional part (which would be assumed when encountering 10.trunc()) and reporting an error. To be clearer, I could place 10. in round brackets, as in (10.).trunc().

When you run these test cases, you should observe the following output:

(25.6).trunc(): 25
(-25.6).trunc(): -25
10..trunc(): 10

Augmenting String

The String object is an object wrapper for strings. I’ve added endsWith(), reverse(), and startsWith() methods that are similar to their Java language counterparts to this object.

Implementing and Testing endsWith() and startsWith()

The following code fragment presents the implemention of endsWith() and startsWith() methods that perform case-sensitive comparisons of a suffix or prefix with the end or start of a string, respectively:

String.prototype.endsWith = 
   function(suffix) 
   {
      if (typeof suffix != "string")
         throw "illegal argument" + suffix;

      if (suffix == "")
         return true;

      var str = this;
      var index = str.length - suffix.length;
      return str.substring(index, index + suffix.length) == suffix;
   };

String.prototype.startsWith = 
   function(prefix)
   {
      if (typeof prefix != "string")
         throw "illegal argument" + prefix;

      if (prefix == "")
         return true;

      var str = this;
      return str.substring(0, prefix.length) == prefix;
   };

Each of endsWith() and startsWith() is similar in that it first verifies that its argument is a string, throwing an exception when this isn’t the case. It then returns true when its argument is the empty string because empty strings always match.

Each method also uses String‘s substring() method to extract the appropriate suffix or prefix from the string before the comparison. However, they differ in their calculations of the start and end indexes that are passed to substring().

The following test cases exercise these methods:

try
{      
   alert("'abc'.endsWith(undefined): " + "abc".endsWith(undefined));
}
catch (err)
{
   alert("not a string");
}
alert("'abc'.endsWith(''): " + "abc".endsWith(""));
alert("'this is a test'.endsWith('test'): " +
      "this is a test".endsWith("test"));
alert("'abc'.endsWith('abc'): " + "abc".endsWith("abc"));
alert("'abc'.endsWith('Abc'): " + "abc".endsWith("Abc"));
alert("'abc'.endsWith('abcd'): " + "abc".endsWith("abcd"));

try
{      
   alert("'abc'.startsWith(undefined): " + "abc".startsWith(undefined));
}
catch (err)
{
   alert("not a string");
}
alert("'abc'.startsWith(''): " + "abc".startsWith(""));
alert("'this is a test'.startsWith('this'): " +
      "this is a test".startsWith("this"));
alert("'abc'.startsWith('abc'): " + "abc".startsWith("abc"));
alert("'abc'.startsWith('Abc'): " + "abc".startsWith("Abc"));
alert("'abc'.startsWith('abcd'): " + "abc".startsWith("abcd"));

When you run these test cases, you should observe the following output:

not a string
'abc'.endsWith(''): true
'this is a test'.endsWith('test'): true
'abc'.endsWith('abc'): true
'abc'.endsWith('Abc'): false
'abc'.endsWith('abcd'): false
not a string
'abc'.startsWith(''): true
'this is a test'.startsWith('this'): true
'abc'.startsWith('abc'): true
'abc'.startsWith('Abc'): false
'abc'.startsWith('abcd'): false

Implementing and Testing reverse()

The following code fragment presents the implemention of a reverse() method that reverses the characters of the string on which this method is called and returns the resulting string:

String.prototype.reverse = 
   function()
   {
      var str = this;
      var revStr = "";
      for (var i = str.length - 1; i >= 0; i--)
         revStr += str.charAt(i);
      return revStr;
   };

reverse() loops over the string backwards and appends each character to a temporary string variable, which is returned. Because string concatenation is expensive, you might prefer an array-oriented expression such as return this.split("").reverse().join("");.

The following test case exercises this method:

alert("'abc'.reverse(): " + "abc".reverse());

When you run this test case, you should observe the following output:

'abc'.reverse(): cba

Conclusion

JavaScript makes it easy to augment its core objects with new capabilities and you can probably think of additional examples.

I find it easiest to place all of a core object’s new property and method definitions in a separate file (e.g., date.js) and include the file in a page’s header via a <script> element (e.g., <script type="text/javascript" src="date.js"><script>).

For homework, add a shuffle() method to the Array object to shuffle an array of elements (e.g., playing card objects). Use this article’s rnd() method in the implementation.

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • Basje

    Extending JavaScript DOM Core Objects is considered bad practice. Please read this article: http://perfectionkills.com/whats-wrong-with-extending-the-dom/

    • Jeff Friesen

      Thanks for this information. However, this article isn’t about extending DOM objects (e.g., Element, Document, and so on), which is what the article references. Instead, it’s about adding a few useful capabilities to the JavaScript built-in (i.e., core) objects: Array, Boolean, Date, Math, Number, String, and RegExp. That’s it.

      One interesting item from the article is the possibility of name collisions. However, the website developer could counter this problem in any number of ways — reversed Internet domain name prefixes and keeping track of what scripts are being included in a web page are two possibilities.

      I agree that extending DOM core objects (e.g., Element and others) is not so good. However, as said before, I’m restricting my discussion to JavaScript built-in objects (e.g., Number and Array).

      All the best.

      Jeff

    • Anonymous

      this article seems from year 2000 … today we have Object.defineProperty/ies to at least avoid enumerability problem, which is one reason also all dogma oriented developers can move forward and pass that scary phase since today things are way better and/or different: http://perfectionkills.com/extending-built-in-native-objects-evil-or-not/ since somebody pointed at kangax, at least the recent post, not the jurassic one from those dark days (where who extended the prototype made the JavaScript better as we know today) Reasonable, flexible, open minded, but also updated, make a developer a better one, imho.

  • Chris C

    I find augmenting the prototype of built-in objects to be very useful. However, I always feel guilty using this technique as it’s often called out as bad practice, because doing so can conflict with third-party scripts, or confuse people who are collaborating on the code. However, seeing as it’s being endorsed by SitePoint, I’m now able to feel more comfortable with modifying the prototype!

    • Jeff Friesen

      I don’t advocate augmenting the prototypes of HTML DOM objects, which is widely considered bad practice. For one thing, there’s no guarantee that DOM object prototypes will be exposed.

      However, I believe that augmenting the prototypes of Array, Boolean, Date, Function (if a good extension can be thought up), Number, RegExp, and String is okay — Math doesn’t expose a prototype property (see http://aptana.com/reference/api/Math.html).

      However, you can still run into name collisions when importing two different libraries that add the same-named property or method to a core object. One way to handle this problem is to prepend a unique prefix to the object’s property and method names. You could use your reversed Internet domain name for this prefix. Another way to handle this problem is to be aware of the libraries being imported into a web page and take appropriate action when you find this happening (you might need to collapse multiple imports that reference a core object into a single import and place all of the changes to that object in a single script library). If you are the only one designing a website, this shouldn’t have to be a difficult task.

      All the best.

      Jeff

  • Anonymous

    Hmm. I love that JavaScript can do this and it’s great for polyfills. For example, String.trim isn’t supported in older browsers so we can add it in.

    However, the practice fell out of favor because of naming collisions. It’s not only libraries which could implement the same properties/methods, but browsers themselves. For example, if Math.toDegrees became an official JavaScript method, your function would override it. Other code expecting to use the standard call could fail.

    Admittedly, it’s a shame – augmentation is powerful and seems so elegant. You may be able to get away with it if you’re in total control of all page code but, in general, the risks outweigh the rewards.

    • Jeff Friesen

      Thanks for pointing this problem out. It has occurred to me since writing the article.

      As David Laberge mentions, the solution to this problem is to test for the existence of the property or method before adding the functionality. However, one could argue that doing so is still problematic because an existing same-named property/method may differ in some respect to the desired property/method. In a worst case scenario, each method/property name to be added could be prefixed with a unique identifier such as a reversed Internet domain name, but the downside to that is having to type a longer identifier.

      All the best.

      Jeff

  • David Laberge

    Here a nice validation we add when we do this kind of technic :
    if (!String.prototype.trim) {
    // add your trimming function
    }

    Doing this allow you to add the functionnality only when it is not already implementing by the browser.

    • Jeff Friesen

      Thanks David. I should have mentioned this in my article but it didn’t occur to me then. As the old maxim states, hindsight is 20/20.

      All the best.

      Jeff

  • Joan

    The approach to augment native JavaScript objects may cause unexpected problems. Remember Prototype.JS?

  • brothercake

    I’m obliged to agree with the other commentators — this is bad practise which most programmers have long abandoned. Augmenting built-in objects is no different than creating global variables, which we try to avoid because they pollute the global scope and waste memory.

  • pepkin88

    To all those wondering whether or not augment host objects, read this:
    http://sugarjs.com/native
    and
    http://agavejs.org/#concerns

  • Anonymous

    May be for an array “equals()” method will be better try to use [].toString() method and compare two strings instead iteration by whole array.

  • Anonymous

    Also will be better if rnd method can take two arguments and return value in that range, like this:
    return Math.floor(Math.random() * (max – min + 1)) + min;