Augmenting JavaScript Core Objects

Share this article

Key Takeaways

  • JavaScript core objects like Array, Boolean, Date, Function, Math, Number, RegExp, and String can be augmented with new properties and methods to enhance functionality.
  • New methods can be added to core objects or their prototypes depending on whether they should apply to the object itself or its instances.
  • Augmentation must be approached with caution to avoid name collisions and potential future conflicts with updates to JavaScript’s standard objects.
  • Specific methods, such as `Array.prototype.equals` and `Array.prototype.fill`, demonstrate practical examples of how to extend core objects to include useful, custom functionality.
  • Testing new methods thoroughly is crucial to ensure that they behave as expected in various scenarios and do not introduce bugs into your code.
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.

Frequently Asked Questions (FAQs) on Augmenting JavaScript Core Objects

What is the Purpose of Augmenting JavaScript Core Objects?

Augmenting JavaScript core objects involves adding new methods or properties to the built-in objects in JavaScript. This is done to extend the functionality of these objects, making them more versatile and useful. For instance, you can add a method to the Array object that allows you to easily find the maximum value in an array. This can save you time and make your code cleaner and more efficient.

How Can I Augment JavaScript Core Objects?

Augmenting JavaScript core objects is done by adding methods or properties directly to the object’s prototype. For example, to add a method to the Array object, you would write Array.prototype.yourMethodName = function() {...}. Inside the function, you can write the code that defines what your method does.

Is it Safe to Augment JavaScript Core Objects?

While augmenting JavaScript core objects can be very useful, it should be done with caution. Overriding built-in methods or properties can lead to unexpected behavior and bugs. It’s generally recommended to only augment core objects when necessary and to always test your code thoroughly.

Can I Remove Augmentations from JavaScript Core Objects?

Yes, you can remove augmentations from JavaScript core objects. This is done by deleting the method or property from the object’s prototype. For example, to remove a method you added to the Array object, you would write delete Array.prototype.yourMethodName.

What are Some Common Uses for Augmenting JavaScript Core Objects?

Augmenting JavaScript core objects can be used for a variety of purposes. Some common uses include adding utility methods to objects like Array or String, creating polyfills for methods not supported in older browsers, and extending objects with custom functionality for specific use cases.

Can I Augment JavaScript Core Objects in All Browsers?

While most modern browsers support augmenting JavaScript core objects, there may be some differences in how this is handled between different browsers. It’s always a good idea to test your code in multiple browsers to ensure it works as expected.

What are the Risks of Augmenting JavaScript Core Objects?

The main risk of augmenting JavaScript core objects is that it can lead to conflicts with other code. If you override a built-in method or property, any code that relies on the original functionality may not work correctly. This is why it’s important to use caution when augmenting core objects.

Can I Augment JavaScript Core Objects with ES6?

Yes, you can augment JavaScript core objects with ES6. In fact, ES6 introduced several new methods for augmenting core objects, making it even easier and more powerful.

What is the Difference Between Augmenting and Extending JavaScript Core Objects?

Augmenting and extending JavaScript core objects are similar in that they both involve adding new functionality to these objects. The main difference is that augmenting involves adding methods or properties directly to the object’s prototype, while extending involves creating a new object that inherits from the core object.

Can I Augment JavaScript Core Objects with TypeScript?

Yes, you can augment JavaScript core objects with TypeScript. TypeScript provides a feature called declaration merging, which allows you to add new methods or properties to existing objects. This can be very useful for augmenting core objects.

Jeff FriesenJeff Friesen
View Author

Jeff Friesen is a freelance tutor and software developer with an emphasis on Java and mobile technologies. In addition to writing Java and Android books for Apress, Jeff has written numerous articles on Java and other technologies for SitePoint, InformIT, JavaWorld, java.net, and DevSource.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week