My recent Augmenting JavaScript Core Objects article showed how to introduce new properties and methods to JavaScript’s Array
, Boolean
, Date
, Math
, Number
, and String
core objects. I followed in the tradition of other articles and blog posts, including those listed below, that show how to extend these core objects with new capabilities:
- Extend Math.round, Math.ceil and Math.floor to allow for precision
- Extending JavaScript Objects and Classes
- Extending JavaScript’s String Object
- Extending The JavaScript Date Object with User Defined Methods
- JavaScript Array Contains
Directly adding properties to a core object or its prototype is controversial. In his Extending JavaScript Natives blog post, Angus Croll addresses several problems with this approach. For example, future browser versions may implement an efficient property or method that gets clobbered by a less efficient custom property/method. Read Croll’s blog post for more information on this and other problems.
Because core object augmentation is powerful and elegant, there should be a way to leverage this feature while avoiding its problems. Fortunately, there is a way to accomplish this task, by leveraging the adapter design pattern, which is also known as the wrapper pattern. In this article, I introduce a new version of my library that uses wrapper to augment various core objects without actually augmenting them.
Exploring a New Core Object Augmentation Library
My new core object augmentation library attempts to minimize its impact on the global namespace by leveraging the JavaScript Module Pattern, which places all library code in an anonymous closure. This library currently exports _Date
and _Math
objects that wrap themselves around Date
and Math
, and is accessed by interrogating the ca_tutortutor_AJSCOLib
global variable.
About ca_tutortutor_AJSCOLib
The ca_tutortutor_AJSCOLib
global variable provides access to the augmentation library. To minimize the chance of a name collision with another global variable, I’ve prefixed AJSCOLib
with my reversed Internet domain name.
Listing 1 presents the contents of my library, which is stored in an ajscolib.js
script file.
var ca_tutortutor_AJSCOLib = (function() { var my = {}; var _Date_ = Date; function _Date(year, month, date, hours, minutes, seconds, ms) { if (year === undefined) this.instance = new _Date_(); else if (month === undefined) this.instance = new _Date_(year); else if (hours === undefined) this.instance = new _Date_(year, month, date); else this.instance = new _Date_(year, month, date, hours, minutes, seconds, ms); this.copy = function() { return new _Date_(this.instance.getTime()); }; this.getDate = function() { return this.instance.getDate(); }; this.getDay = function() { return this.instance.getDay(); }; this.getFullYear = function() { return this.instance.getFullYear(); }; this.getHours = function() { return this.instance.getHours(); }; this.getMilliseconds = function() { return this.instance.getMilliseconds(); }; this.getMinutes = function() { return this.instance.getMinutes(); }; this.getMonth = function() { return this.instance.getMonth(); }; this.getSeconds = function() { return this.instance.getSeconds(); }; this.getTime = function() { return this.instance.getTime(); }; this.getTimezoneOffset = function() { return this.instance.getTimezoneOffset(); }; this.getUTCDate = function() { return this.instance.getUTCDate(); }; this.getUTCDay = function() { return this.instance.getUTCDay(); }; this.getUTCFullYear = function() { return this.instance.getUTCFullYear(); }; this.getUTCHours = function() { return this.instance.getUTCHours(); }; this.getUTCMilliseconds = function() { return this.instance.getUTCMilliseconds(); }; this.getUTCMinutes = function() { return this.instance.getUTCMinutes(); }; this.getUTCMonth = function() { return this.instance.getUTCMonth(); }; this.getUTCSeconds = function() { return this.instance.getUTCSeconds(); }; this.getYear = function() { return this.instance.getYear(); }; this.isLeap = function() { var year = this.instance.getFullYear(); return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0); }; _Date.isLeap = function(date) { if (date instanceof _Date) date = date.instance; var year = date.getFullYear(); return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0); }; this.lastDay = function() { return new _Date_(this.instance.getFullYear(), this.instance.getMonth() + 1, 0).getDate(); }; _Date.monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; _Date.parse = function(date) { if (date instanceof _Date) date = date.instance; return _Date_.parse(date); }; this.setDate = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setDate(date); }; this.setFullYear = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setFullYear(date); }; this.setHours = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setHours(date); }; this.setMilliseconds = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setMilliseconds(date); }; this.setMinutes = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setMinutes(date); }; this.setMonth = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setMonth(date); }; this.setSeconds = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setSeconds(date); }; this.setTime = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setTime(date); }; this.setUTCDate = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCDate(date); }; this.setUTCFullYear = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCFullYear(date); }; this.setUTCHours = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCHours(date); }; this.setUTCMilliseconds = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCMilliseconds(date); }; this.setUTCMinutes = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCMinutes(date); }; this.setUTCMonth = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCMonth(date); }; this.setUTCSeconds = function(date) { if (date instanceof _Date) date = date.instance; this.instance.setUTCSeconds(date); }; this.toDateString = function() { return this.instance.toDateString(); }; this.toISOString = function() { return this.instance.toISOString(); }; this.toJSON = function() { return this.instance.toJSON(); }; this.toLocaleDateString = function() { return this.instance.toLocaleDateString(); }; this.toLocaleTimeString = function() { return this.instance.toLocaleTimeString(); }; this.toString = function() { return this.instance.toString(); }; this.toTimeString = function() { return this.instance.toTimeString(); }; this.toUTCString = function() { return this.instance.toUTCString(); }; _Date.UTC = function(date) { if (date instanceof _Date) date = date.instance; return _Date_.UTC(date); }; this.valueOf = function() { return this.instance.valueOf(); }; } my._Date = _Date; var _Math = {}; var props = Object.getOwnPropertyNames(Math); props.forEach(function(key) { if (Math[key]) _Math[key] = Math[key]; }); if (!_Math.GOLDEN_RATIO) _Math.GOLDEN_RATIO = 1.61803398874; if (!_Math.rnd || _Math.rnd.length != 1) _Math.rnd = function(limit) { if (typeof limit != "number") throw "illegal argument: " + limit; return Math.random() * limit | 0; }; if (!_Math.rndRange || _Math.rndRange.length != 2) _Math.rndRange = function(min, max) { if (typeof min != "number") throw "illegal argument: " + min; if (typeof max != "number") throw "illegal argument: " + max; return Math.floor(Math.random() * (max - min + 1)) + min; }; if (!_Math.toDegrees || _Math.toDegrees.length != 1) _Math.toDegrees = function(radians) { if (typeof radians != "number") throw "illegal argument: " + radians; return radians * (180 / Math.PI); }; if (!_Math.toRadians || _Math.toRadians.length != 1) _Math.toRadians = function(degrees) { if (typeof degrees != "number") throw "illegal argument: " + degrees; return degrees * (Math.PI / 180); }; if (!_Math.trunc || _Math.trunc.length != 1) _Math.trunc = function(n) { if (typeof n != "number") throw "illegal argument: " + n; return (n >= 0) ? Math.floor(n) : -Math.floor(-n); }; my._Math = _Math; return my; }());
Listing 1: This self-contained augmentation library can be extended to support all core objects
All variables and functions declared within the anonymous closure are local to that closure. To be accessed from outside the closure, a variable or function must be exported. To export the variable or function, simply add it to an object and return that object from the closure. In Listing 1, the object is known as my
and is assigned a _Date
function reference and a _Math
object reference.
Following the declaration of variable my
, which is initialized to an empty object, Listing 1 declares variable _Date_
, which references the Date
core object. Wherever I need to access Date
from within the library, I refer to _Date_
instead of Date
. I’ll explain my reason for this arrangement later in this article.
Listing 1 now declares a _Date
constructor for constructing _Date
wrapper objects. This constructor declares the same year
, month
, date
, hours
, minutes
, seconds
, and ms
parameters as the Date
core object. These parameters are interrogated to determine which variant of the Date
constructor to invoke:
-
_Date()
invokesDate()
to initialize aDate
object to the current date. This scenario is detected by testingyear
forundefined
. -
_Date(year)
invokesDate(milliseconds)
orDate(dateString)
to initialize aDate
object to the specified number of milliseconds or date string — I leave it toDate
to handle either case. This scenario is detected by testingmonth
forundefined
. -
_Date(year, month, date)
invokes_Date(year, month, date)
to initialize aDate
object to the specified year, month, and day of month (date). This scenario is detected by testinghour
forundefined
. -
_Date(year, month, day, hours, minutes, seconds, milliseconds)
invokesDate(year, month, day, hours, minutes, seconds, milliseconds)
to initialize aDate
object to the date described by the individual components. This scenario is the default.
Regardless of which constructor variant (a constructor invocation with all or fewer arguments) is invoked, the returned result is stored in _Date
‘s instance
property. You should never access instance
directly because you may need to rename this property should Date
introduce an instance
property in the future. Not accessing instance
outside of the library reduces code maintenance.
At this point, Listing 1 registers new copy()
, isLeap()
, and lastDay()
methods, and a new monthNames
property with _Date
. It also registers Date
‘s methods. The former methods augment Date
with new functionality that’s associated with _Date
instead of Date
, and are described below. The latter methods use instance
to access the previously stored Date
instance, usually to invoke their Date
counterparts:
-
copy()
creates a copy of the instance of theDate
object that invokes this method. In other words, it clones theDate
instance. Example:var d = new Date(); var d2 = d.copy();
-
isLeap()
returns true when the year portion of the invokingDate
object instance represents a leap year; otherwise, false returns. Example:var d = new Date(); alert(d.isLeap());
-
isLeap(date)
returns true when the year portion ofdate
represents a leap year; otherwise, false returns. Example:alert(Date.isLeap(new Date()));
-
lastDay()
returns the last day in the month of the invokingDate
object instance. Example:var d = new Date(); alert(d.lastDay());
-
Although not a method, you can obtain an English-based long month name from the
Date.monthNames
array property. Pass an index ranging from 0 through 11. Example:alert(Date.monthNames[0])
Methods that are associated with _Date
instead of its instances are assigned directly to _Date
, as in _Date.UTC = function(date)
. The date
parameter identifies either a core Date
object reference or a _Date
reference. Methods that are associated with _Date
instances are assigned to this
. Within the method, the Date
instance is accessed via this.instance
.
You would follow the previous protocol to support Array
, String
, and the other core objects — except for Math
. Unlike the other core objects, you cannot construct Math
objects. Instead, Math
is simply a placeholder for storing static properties and methods. For this reason, I treat Math
differently by declaring a _Math
variable initialized to the empty object and assigning properties and methods directly to this object.
The first step in initializing _Math
is to invoke Object
‘s getOwnPropertyNames()
method (implemented in ECMAScript 5 and supported by modern desktop browsers) to return an array of all properties (enumerable or not) found directly upon the argument object, which is Math
. Listing 1 then assigns each property (function or otherwise) to _Math
before introducing new properties/methods (when not already present):
-
GOLDEN_RATIO
is a constant for the golden ratio that I mentioned in my previous article. Example:alert(Math.GOLDEN_RATIO);
-
rnd(limit)
returns an integer ranging from 0 through one less thanlimit
‘s value. Example:alert(Math.rnd(10));
-
rndRange(min, max)
returns a random integer ranging frommin
‘s value throughmax
‘s value. Example:alert(Math.rndRange(10, 20));
-
toDegrees(radians)
converts theradians
value to the equivalent value in degrees and returns this value. Example:alert(Math.toDegrees(Math.PI));
-
toRadians(degrees)
converts thedegrees
value to the equivalent value in radians and returns this value. Example:alert(Math.toRadians(180));
-
trunc(n)
removes the fractional part from the positive or negative number passed ton
and returns the whole part. Example:alert(Math.trunc(5.8));
Each method throws an exception signifying an illegal argument when it detects an argument that’s not of Number
type.
Why bother creating an augmentation library instead of creating separate utility objects (such as DateUtil
or MathUtil
)? The library serves as a massive shim to provide consistent functionality across browsers. For example, Firefox 25.0’s Math
object exposes a trunc()
method whereas this method is absent from Opera 12.16. My library ensures that a trunc()
method is always available.
Testing and Using the New Core Object Augmentation Library
Now that you’ve had a chance to explore the library, you’ll want to try it out. I’ve created a pair of scripts that test various new _Date
and _Math
capabilities, and have created a pair of more practical scripts that use the library more fully. Listing 2 presents an HTML document that embeds a script for testing _Date
.
<!DOCTYPE html> <html> <head> <title> Augmented Date Tester </title> <script type="text/javascript" src="ajscolib.js"> </script> </head> <body> <script> var Date = ca_tutortutor_AJSCOLib._Date; var date = new Date(); alert("Current date: " + date); alert("Current date: " + date.toString()); var dateCopy = date.copy(); alert("Copy of current date: " + date.toString()); alert("Current date == Copy of current date: " + (date == dateCopy)); alert("Isleap " + date.toString() + ": " + date.isLeap()); alert("Isleap July 1, 2012: " + Date.isLeap(new Date(2012, 6, 1))); alert("Last day: "+ date.lastDay()); alert("Month names: " + Date.monthNames); </script> </body> </html>
Listing 2: Testing the “augmented” Date
object
When you work with this library, you won’t want to specify ca_tutortutor_AJSCOLib._Date
and probably won’t want to specify _Date
. Instead, you’ll want to specify Date
as if you’re working with the core object itself. You shouldn’t have to change your code to change Date
references to something else. Fortunately, you don’t have to do that.
The first line in the script assigns ca_tutortutor_AJSCOLib._Date
to Date
, effectively removing all access to the Date
core object. This is the reason for specifying var _Date_ = Date;
in the library. If I referred to Date
instead of _Date_
in the library code, you would observe “too much recursion” (and probably other problems).
The rest of the code looks familiar to those who’ve worked with Date
. However, there’s a small hiccup. What gets output when you invoke alert("Current date: " + date);
? If you were using the Date
core object, you would observe Current date:
followed by a string representation of the current date. In the current context, however, you observe Current date:
followed by a numeric milliseconds value.
toString()
versus valueOf()
Check out Object-to-Primitive Conversions in JavaScript to learn why alert("Current date: " + date);
results in a string or numeric representation of date
.
Let’s put the “augmented” Date
object to some practical use, such as creating a calendar page. The script will use document.writeln()
to output this page’s HTML based on the <table>
element. Two variants of the _Date
constructor along with the getFullYear()
, getMonth()
, getDay()
, lastDay()
, and getDate()
methods, and the monthNames
property will be used. Check out Listing 3.
<!DOCTYPE html> <html> <head> <title> Calendar </title> <script type="text/javascript" src="ajscolib.js"> </script> </head> <body> <script> var Date = ca_tutortutor_AJSCOLib._Date; var date = new Date(); var year = date.getFullYear(); var month = date.getMonth(); document.writeln("<table border=1>"); document.writeln("<th bgcolor=#eeaa00 colspan=7>"); document.writeln("<center>" + Date.monthNames[month] + " " + year + "</center>"); document.writeln("</th>"); document.writeln("<tr bgcolor=#ff7700>"); document.writeln("<td><b><center>S</center></b></td>"); document.writeln("<td><b><center>M</center></b></td>"); document.writeln("<td><b><center>T</center></b></td>"); document.writeln("<td><b><center>W</center></b></td>"); document.writeln("<td><b><center>T</center></b></td>"); document.writeln("<td><b><center>F</center></b></td>"); document.writeln("<td><b><center>S</center></b></td>"); document.writeln("</tr>"); var dayOfWeek = new Date(year, month, 1).getDay(); var day = 1; for (var row = 0; row < 6; row++) { document.writeln("<tr>"); for (var col = 0; col < 7; col++) { var row; if ((row == 0 && col < dayOfWeek) || day > date.lastDay()) { document.writeln("<td bgcolor=#cc6622>"); document.writeln(" "); } else { if (day == date.getDate()) document.writeln("<td bgcolor=#ffff00>"); else if (day % 2 == 0) document.writeln("<td bgcolor=#ff9940>"); else document.writeln("<td>"); document.writeln(day++); } document.writeln("</td>"); } document.writeln("</tr>"); } document.writeln("</table>"); </script> </body> </html>
Listing 3: Using the “augmented” Date
object to generate a calendar page
To create a realistic calendar page, we need to know on which day of the week the first day of the month occurs. Expression new Date(year, month, 1).getDay()
gives us the desired information (0 for Sunday, 1 for Monday, and so on), which is assigned to dayOfWeek
. Every square on the top row whose column index is less than dayOfWeek
is left blank.
Figure 1 shows a sample calendar page.
![The current day is highlighted in yellow.](https://uploads.sitepoint.com/wp-content/uploads/2013/11/ajscorfig1.png)
Figure 1: The current day is highlighted in yellow.
Listing 4 presents an HTML document that embeds a script for testing _Math
.
<!DOCTYPE html> <html> <head> <title> Augmented Math Tester </title> <script type="text/javascript" src="ajscolib.js"> </script> </head> <body> <script> var Math = ca_tutortutor_AJSCOLib._Math; 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)); for (var i = 0; i < 10; i++) alert(Math.rndRange(5, 9)); 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)); </script> </body> </html>
Listing 4: Testing the “augmented” Math
object
Let’s put the “augmented” Math
object to some practical use, such as displaying a cardioid curve, which is a plane curve traced by a point on the perimeter of a circle that’s rolling around a fixed circle of the same radius. The script will use Math
‘s rndRange()
, toRadians()
, cos()
, and sin()
methods. Check out Listing 5.
<!DOCTYPE html> <html> <head> <title> Cardioid </title> <script type="text/javascript" src="ajscolib.js"> </script> </head> <body> <canvas id="canvas" width="300" height="300"> canvas not supported </canvas> <script> var Math = ca_tutortutor_AJSCOLib._Math; var canvas = document.getElementById("canvas"); var canvasctx = canvas.getContext("2d"); var width = document.getElementById("canvas").width; var height = document.getElementById("canvas").height; canvasctx.fillStyle = "#000"; canvasctx.fillRect(0, 0, width, height); canvasctx.fillStyle = "RGB(" + Math.rndRange(128, 255) + "," + Math.rndRange(128, 255) + "," + Math.rndRange(128, 255) + ")"; canvasctx.beginPath(); for (var angleDeg = -180.0; angleDeg < 180.0; angleDeg += 0.1) { var angle = Math.toRadians(angleDeg); // Evaluate cardioid curve equation. This produces radius for // given angle. Note: [r, angle] are the polar coordinates. var r = 60.0 + 60.0 * Math.cos(angle); // Convert polar coordinates to rectangular coordinates. Add // width / 2 and height / 2 to move curve's origin to center // of canvas. (Origin defaults to canvas's upper-left corner.) var x = r * Math.cos(angle) + width / 2; var y = r * Math.sin(angle) + height / 2; if (angle == 0.0) canvasctx.moveTo(x, y); else canvasctx.lineTo(x, y) } canvasctx.closePath(); canvasctx.fill(); </script> </body> </html>
Listing 5: Using the “augmented” Math
object to generate a cardioid curve
Listing 5 uses HTML5’s canvas element and API to present the cardioid curve, which is constructed as a polygon via the canvas context’s beginPath()
, moveTo()
, lineTo()
, and closePath()
methods. Each component of the curve’s fill color is randomly chosen via rndRange()
. Its arguments ensure that the component isn’t too dark. The curve is filled via the canvas context’s fill()
method.
Figure 2 shows a colorful cardioid curve.
Figure 2: Reload the page to change the curve’s color.
Conclusion
This article showed how to create a library that augments JavaScript’s core objects without augmenting them directly. The library’s public interface is portable across browsers, although it’s possible that the implementation might need adjusting for compatibility, performance, or other reasons. As an exercise, add my previous augmentation article’s Array
, Boolean
, Number
, and String
enhancements to this library.