One should neither split infinitives nor end sentences with prepositions. These statements are examples of prescriptive grammar. Prescriptive grammar means rules invented by self-appointed experts who presume to tell others how they should speak if they don’t want to sound like louts. As far as I can tell, the origin of these prescriptions is that you couldn’t split infinitives or strand prepositions in Latin. Except we speak English and English accommodates “to boldly go” just fine and if we were to ask someone “About what are you talking?” they would be unlikely to compliment us on the quality of our speech.
The Internet is full of self-appointed experts who presume to tell others how to program in a similarly prescriptive way. One of the prescriptions that you may or may not be aware of is that one should never extend the built-in JavaScript classes (i.e. String, Array, Date, etc.). This Stack Overflow post is a good example of the controversy complete with huffing and puffing and stern warnings against the evils of such a filthy habit. Yet the ability to extend the behaviour of classes without subclassing is a core feature of JavaScript, not a bug, so a prescription against such a useful ability had better have a really good rationale (and no, MDN, fulmination is not a valid argument). The arguments against go something like this:
- The method might be implemented in a future standard in a slightly different way
- Your method name appears in
for...in
loops - Another library you use might implement the same method
- There are alternative ways of doing it
- It’s acceptable practice for certain elites but not for the unwashed masses
- A newbie programmer might be unaware that the method is non-standard and get confused.
The retort to that last argument is “git gud”, while the retort to the last but one is something rather more robust. We’ll examine the other arguments in turn.
Implemented in a Future Standard
It might happen, particularly if the functionality is obviously useful. I started writing JavaScript early in 2005 and it didn’t take me long to get fed up with doing this:
var a = "...", s = "..."; ... if (a.substr(0, s.length) === s) { // a starts with s }
The obvious solution was to extend the
String
class:String.prototype.startsWith = function(s) { return (this.substr(0, s.length) === s); };
This allowed me to express myself rather more clearly:
if (a.startsWith(s)) { ... }
This is obvious functionality implemented in an obvious way. No, my implementation didn’t test for pathological input but then I never fed it pathological input so it didn’t matter. Sometime around 2012, browsers containing a native implementation of
startsWith
started appearing and the only thing that needed changing was a test for the presence of the method before stomping over it. Otherwise, despite the addition of a feature-in-search-of-a-use-case (a start position other than 0), the official implementations behaved identically to my venerable extension.
For argument’s sake, let’s say I’d called the method beginsWith
. The fix would take longer to type than to apply:
$ sed -i -e 's/beginsWith/startsWith/g;' *.js
Even if I’d been stupid enough to do something like return 0/-1
rather than true/false
, fixing up my uses of startsWith
to use a different return type would have been the work of a few minutes. We call this refactoring which means that the argument in favour of possible future implementations is actually an argument against refactoring which is no argument at all.
for…in Pollution
The problem here is that your method name appears in the enumerable properties of an object but this is actually a non-issue. Leaving aside the fact that it has been possible for some years to ensure that your method is not enumerable in the great majority of browsers in the world, would you ever enumerate the properties of a String
or Date
object? Hint: there are no enumerable properties. Furthermore, if you enumerate over an Array
with a for...in
loop, you’re doing it wrong.
Implemented by Another Library
The problem here is that the last library included wins and if utility library A behaves differently to utility library B, code may break. But again, this is something of a non-issue if certain safeguards are observed: test for the existence of an existing method and implement obvious functionality in an obvious way. If anything, this is an argument against using the monolithic jqueries of the world in favour of micro frameworks, since micro frameworks tend not to do a 1001 things that you don’t know about.
It’s Unnecessary
True, inasmuch as there is always more than one way to do anything. The question is: are alternatives better? Let’s say that I want a function to shuffle an Array. The function might look like this:
function shuffle(a) { var l = a.length, c = l - 1, t, r; for (; c > 0; --c) { r = Math.floor(Math.random() * l); t = a[c]; a[c] = a[r]; a[r] = t; } return a; }
For the interested, this is the Fisher-Yates algorithm. How are we going to make this function available to our application? We can reject one possibility - subclassing
Array
- right away. The reason is that arrays are almost always instantiated using the more expressive literal notation:var a = [1, 2, 3], // like this almost always b = new Array(); // like this almost never
This makes using a
MyExtendedArray
class unfeasible - the overhead would simply be too great. The same applies to theoretical subclasses of String
and Number
.
Using a procedural approach is doable but we now come up against the problem of namespacing. As written, the identifier "shuffle" is placed into the global namespace. This isn't as fatal as some would have you believe if it's for private consumption. However, if you plan to create a library for consumption by others you should avoid using the global namespace because collision with identically named entities is a non-negligible risk. One could imagine, for example, another "shuffle" function that works on a "DeckOfCards" object or a "Songlist". Utility functions are problematic because the namespace isn't obvious. One could choose something like "ArrayUtils" but then you're liable to be competing with everyone else's ArrayUtils. So you might end up doing this:
if (!("CCWUtils" in window)) { CCWUtils = {}; } CCWUtils.Array = { shuffle: function(a) { ... } }; ... var cards = ["AS", "2S", "3S", ... , "JC", "QC", "KC"]; CCWUtils.Array.shuffle(cards);
Remember that we're doing it this way because we believe that adding a "shuffle" method to the native Array class is somehow sinful. If that feels like the tail wagging the dog, compare the sinful approach:
if (!("shuffle" in Array.prototype)) { Array.prototype.shuffle = function() { ... }; } ... var cards = ["AS", "2S", "3S", ... , "JC", "QC", "KC"]; cards.shuffle();
To my eyes,
cards.shuffle()
is both more idiomatic and more elegant. Namespacing isn't a problem and I take care to play nicely with any existing shuffle method.
Doing it Right
I believe that extending built-in classes is a valid practice but there are some guidelines that you might follow:
- Add useful functionality. For example, the
concat
method of String objects implements the same functionality as the+
operator. Don't do something similar - Use an obvious name and implement obvious functionality
- Be careful to avoid overwriting a method with the same name
- If possible, ensure that your method is non-enumerable, if only to silence critics who might otherwise complain that you're polluting their
for...in
loops - Take some pains to ensure that your method behaves like other methods. For example, methods of built-in objects are typically implemented generically and mutator methods (methods that modify the object) typically return the mutated object as well.
With that in mind, here is the complete shuffle extension:
(function() { "use strict"; var _shuffle = function(a) { if (null == a) { throw new TypeError("can't convert " + a + " to an object"); } var _a = Object(a), len = _a.length >>> 0, c = len - 1, t, r; for (; c > 0; --c) { r = Math.floor(Math.random() * len); // Swap the item at c with that at r t = _a[c]; _a[c] = _a[r]; _a[r] = t; } return _a; }, obj = Array.prototype, mName = "shuffle", m = function() { return _shuffle(this); }; if (!(mName in obj)) { try { Object.defineProperty(obj, mName, { enumerable : false, value : m }); } catch(e) { obj[mName] = m; } } })();
Note the following:
- Lines 4-6: Following other Array methods, raise a TypeError if passed
undefined
ornull
. And yes, that is==
rather than===
since I don't care to distinguish the two - Line 7: Our method can work generically on array-like objects (objects with a
length
and numeric property names) and it won't barf if passed a primitive value. You can pass non-Array objects to the method by invoking it asArray.prototype.shuffle.call(obj)
. Note, however, that aTypeError
will be raised if the method is passed an immutable object, such as aString
. That is also true of, say,reverse
so it's not a problem - Line 15: Our method returns the mutated Object in the manner of similar methods such as
sort
andreverse
- Line 17: Use the obvious name "shuffle". If we used something like "$shuffle" to ensure that we didn't conflict with a future implementation, we wouldn't automatically benefit from the native implementation. As it is, if "shuffle" ever becomes a standard method on Array, our extension simply becomes a shim
- Line 19: Don't overwrite an existing method
- Lines 21-24: This is how you ensure that the new method is not an enumerable property
- Line 27: Fallback position for the off-chance that our code is running in Internet Explorer <= 8 or something similarly inadequate.
But Object.prototype is Forbidden, Right?
I said earlier that forbidding the use of a useful feature needed a good rationale. Well, here goes. Imagine that I wanted a method to count the number of properties of an Object and I implemented it like this:
Object.prototype.count = function() { var p, ret = 0; for (p in this) { if (this.hasOwnProperty(p)) { ++ret; } } return ret; };
The problem is that Object is a first-class data type used to store key-value pairs which means that I can also do this:
var stat = { value: 1, count: 4 // Oops! count method is now shadowed };
Rather than adding value, my
count
method has stolen a perfectly good property name. Even if we think we can do what we want by picking really obscure method names (breaking the "obvious" guideline in the process), Object
is the base class of everything and the practice is simply hazard-prone. Illustratively, adding methods directly to Object.prototype
breaks the "behaves like other methods" guideline as well, since most Object
methods are static:var a = 1; ... if (Object.is(a, 1)) { // Not "if (a.is(1))" }
So, yes,
Object.prototype
is forbidden.
Erm, What About "Host" Objects?
By "host" objects, we mean the objects that the browser makes available to your script that represent the visible and not-so-visible parts of your user interface: Document
, Element
, Node
, Event
and so on. These were the subject of an essay some years ago. Most of the issues considered are no longer really issues (applet elements, anyone?) and now this is a reasonable strategy:
if (!("Element" in window)) { // Don't try and do anything at all }
With that in place, I can't see that the sky would fall if you did something like this:
Element.prototype.show = function(style) { // e.g. "inline-block" style = style || ""; this.style.display = style; }; Element.prototype.hide = function() { this.style.display = "none"; };
A possible problem might be one of scope. The behaviours that a String object doesn't have that you wish it did are distinctly finite: while you might want it to return a reversed copy of itself, you're probably not hankering for it to calculate a SHA256 hash of itself. The behaviours that we might want to add to objects that represent interface elements are not so limited. For example, we might want a method to set colours. Does our setColor
method take two arguments and set the background colour as well? Or does it simply set the foreground colour, leaving the background colour to be specified by a call to setBackgroundColor
? What about a method to set position and z-order? You'll quickly find yourself in danger of violating both the "useful" and "obvious" guidelines.
I'm a great believer in the Unix philosophy of doing one thing well. User-interface toolkits have a bad habit of trying to do flipping everything. My personal feeling is that extending interface objects is a bit like pulling a thread on your jumper: at some point you're going to wish you hadn't started. But if it works for you who am I to say you mustn't?
Conclusions
TL;DR: my views on extending built-in classes are:
Array
,String
,Number
,Date
: Yes; apply common senseObject.prototype
: Seriously, no- "Host" objects: not to my taste, but if it floats your boat...