“Successfull” Subclassing JavaScript’s internal Date class
The Problem
Recently, I have been doing some in-browser automated testing with Testem and Mocha. For continuous integration purposes we are using the PhantomJS headless browser.
All was fine until one day when—boom—a bunch of new tests, which I had written and debugged in Chrome and FireFox, were failing in CI under Phantom. It turns out that Phantom 1.9 has some date parsing issues and cannot parse dates of the form “2011 Feb 09 12:39:09.”
We had previously used a ISO-8601 polyfill to fix a similar problem with legacy Firefox browsers. This problem was a little different. Our date format, though understood by most browsers, isn’t ISO-8601 compliant. More importantly, the polyfill only fixed the “static” Date.parse method. In the current case, the date was being parsed by the Date constructor.
The ISO-8601 polyfill explicitly avoided dealing with the constructor and for good reason. Though I did not know it at the time, JavaScript never intended for anyone to subclass its internal Date type. (More on this later.) Changing our code over to use Date.parse and modifying the polyfill would mean touching a lot of code, and making it more complex, just for CI—something I didn’t want to do. So I set out to write my own polyfill that would handle the constructor as well as Date.parse.
Initial Attempts: Subclassing Date
My initial simple-minded approach was to just subclass Date and override its constructor, like so:
(function () { function fixStringDate (sDate) { //Implementation omitted for brevity return sDate; } Date = (function (JSDate) { function ctor() { this.constructor = newDate; } ctor.prototype = JSDate.prototype; newDate.prototype = new ctor(); function newDate() { if (arguments.length === 1 && typeof arguments[0] === "string") { JSDate.prototype.constructor.call(null, fixStringDate(arguments[0])); } else { JSDate.prototype.constructor.apply(null, arguments); } } newDate.parse = function (sDate) { return JSDate.parse(fixStringDate(sDate)); } return newDate; })(Date) })(); |
As you can see, this is pretty much boilerplate code for creating a subclass in JavaScript. And it seemed to work. After running the code, calls like new Date(‘2011 Feb 09 12:39:09’) no longer threw errors in PhantomJS. That’s the good part.
The bad part is that calling any instance method of the object so created results in a TypeError with a message to the effect of “not a date object”. This stackoverflow question outlines the issue very well. In short, Date isn’t really so much a class as a collection of static methods that only allow themselves to be called with an object whose immediate type is Date. Mere sub-types of Date don’t pass muster.
So the obvious thing to do was to create a real date object internal to the new class and delegate all method calls down to it. Thus:
function newDate() { if (arguments.length === 1 && typeof arguments[0] === "string") { JSDate.prototype.constructor.call(null, fixStringDate(arguments[0])); this.__realDateObject = new JSDate(fixStringDate(arguments[0])) } else { JSDate.prototype.constructor.apply(null, arguments); this.__realDateObject = JSDate.apply(null, arguments); } } var functions = ["getDate", "getDay", ... "valueOf"] for (var i = 0; i < functions.length; i++) { (function (funcName) { newDate.prototype[funcName] = function () { return JSDate.prototype[funcName].apply(this.__realDateObject, arguments); }; })(functions[i]); } |
This solved the problem for our specific cases and I could have stopped here. We fortunately never invoked the constructor with more than one argument.
Since we might do otherwise in the future, I wrote some tests to cover all the cases. In doing so, I found out that calling apply on JSDate (a reference to the original Date class) does not work. It runs, but the object returned is “not a Date object.”
I messed around with a lot of ways to invoke apply on the JSDate constructor including several worthy of mention. None worked in this case. Ultimately I had to resort to brute force, relying on the fact that the Date constructor accepts a reasonably finite number of arguments:
function newDate() { if (arguments.length === 1 && typeof arguments[0] === "string") { JSDate.prototype.constructor.call(null, fixStringDate(arguments[0])); this.__realDateObject = new JSDate(fixStringDate(arguments[0])) } else { JSDate.prototype.constructor.apply(null, arguments); if (arguments.length == 1) this.__realDateObject = new JSDate(arguments[0]); else if (arguments.length == 2) this.__realDateObject = new JSDate(arguments[0], arguments[1]); //etc... } } |
At this point it became obvious that newDate isn’t really acting as a subclass at all. It is acting more like a decorator around the Date type. So all the subclassing code can be removed, which gets us to:
(function () { function fixStringDate (sDate) { //Implementation omitted for brevity return sDate; } Date = (function (JSDate) { function newDate() { if (arguments.length === 1 && typeof arguments[0] === "string") { this.__realDateObject = new JSDate(fixStringDate(arguments[0])) } else { if (arguments.length == 1) this.__realDateObject = new JSDate(arguments[0]); else if (arguments.length == 2) this.__realDateObject = new JSDate(arguments[0], arguments[1]); //etc... } } var functions = ["getDate", "getDay", ... "valueOf"] for (var i = 0; i < functions.length; i++) { (function (funcName) { newDate.prototype[funcName] = function () { return JSDate.prototype[funcName].apply(this.__realDateObject, arguments); }; })(functions[i]); } newDate.parse = function (sDate) { return JSDate.parse(fixStringDate(sDate)); } return newDate; })(Date) })(); |
The Final Solution
Having done this, I realized I was making everything much harder than it needed to be. If I am not actually subclassing Date and just decorating it, all I really need to do is decorate the constructor and return a real Date object from it. Doing so lets me drop all that messy delegation code for the instance methods. Finally my code becomes simple and clean:
(function () { function fixStringDate (sDate) { //Implementation omitted for brevity return sDate; } Date = (function (JSDate) { function newDate() { var theDate; if (arguments.length === 1 && typeof arguments[0] === "string") { theDate = new JSDate(fixStringDate(arguments[0])) } else { if (arguments.length == 1) theDate = new JSDate(arguments[0]); else if (arguments.length == 2) theDate = new JSDate(arguments[0], arguments[1]); //etc... } return theDate; } newDate.parse = function (sDate) { return JSDate.parse(fixStringDate(sDate)); } return newDate; })(Date) })(); |
Intuition tells me this code has some drawbacks that could merit a return to the subclassing solution for certain edge cases. So I am glad to have gone through the whole learning experience. Yet for now, the simpler solution is enough. YAGNI
Full code with tests is available on GitHub.