Extending built in objects in a safer way in Javascript
In Javascript, its very easy to extend built in objects like Object, Array and String. You might find yourself wanting string methods that aren’t built into the String object, and it’s much nicer to read “one short sentence”.toUpperFirst(), than toUpperFirst(“one short sentence”). The problem is that by extending the builtin objects, you are adding global symbols. Global symbols carry much of the same problems of global variables; you risk other parts of the system defining a global symbol with the same name. You might add the “toUpperFirst” function to the String object, and along comes another developer on your project who also needs to add toUpperFirst to the String object. Now, your implementation only changes the first char in the string and leaves the rest untouched, but the other developer decides that it’s a good idea to make the rest of the string lower case. Both yours and his code will work, most of the time, but because of the difference in your implementations, wierd bugs that just “shouldn’t happen” will occur.
In .NET, there is something called Extension Methods, which are basically the same as extending a builtin object in Javascript. It might look something like this:
using System;
namespace MyAppExtensions
{
static public class Extensions
{
public static string ToUpperFirst(this String s)
{
return s.Substring(0).ToUpper() + s.Substring(1);
}
}
}
using System;
using MyAppExtensions;
namespace MyApp
{
public class SomeClass
{
public SomeClass()
{
var myString = "one short sentence".ToUpperFirst();
}
}
}
The key thing here is that you can hide your extension methods in a specific namespace, here called MyAppExtensions. So if anyone wants to use this extension, they need to add “using MyAppExtensions;” in their code. So it doesn’t pollute the global namespace, which is good. You also get a compile-time error if you use two namespaces which define the same extension method.
But you can achive basically the same thing in Javascript by using closures. Let’s say that you’re writing a game where you need to pick a random element from an array often, and you would really like to be able to do:
[1, 2, 3].random()
You could just add random to Array.prototype, but you know the risks of it, so you don’t. Instead, you can do something like this:
var extendArrayWithRandom = function(fn) {
if (typeof Array.prototype.random != "undefined") {
throw "Cannot redefine Array.prototype.random";
}
var random = function() {
var key = Math.round(Math.random() * (this.length - 1));
return this[key];
}
Array.prototype.random = random;
fn();
if (Array.prototype.random !== random) {
throw "Array.prototype.random was redefined";
}
Array.prototype.random = undefined;
}
And use it like this:
extendArrayWithRandom(function() {
console.log([1, 2, 3, 4, 5].random()); // will output a number between 1 and 5
});
console.log([1, 2, 3, 4, 5].random()); // will throw an error because
// random is no longer a method on
// the array prototype
This way, [].random will only exist inside the function that you pass into extendArrayWithRandom(), and if anyone else has defined a global random function for Array, an error is thrown so we don’t risk any confusion. It also checks that noone has redefined Array.prototype.random during the execution of our function. This because it would possibly produce even more confusing results if someone defined global extension during run time without checking if one already exists.
Callbacks
As mentioned in the comments below, this gets a little tricky if you combine it with callbacks/asynchronous events. Take this for example:
extendArrayWithRandom(function() {
loadJson("/settings", function(settings) {
var rand = settings.random();
});
});
Now assuming that loadJson does an Ajax request, the function that we pass into loadJson will execute later than the one we pass into extendArrayWithRandom. This means that Array.prototype.random is no longer available in the Ajax callback, because as soon as the function we pass into extendArrayWithRandom is finished, we remove our implementation of Array.prototype.random.
To get around it, we need to be able to let extendArrayWithRandom return a new function. Something like this:
var extendArrayWithRandom = function(fn) {
var random = function() {
var key = Math.round(Math.random() * (this.length - 1));
return this[key];
}
return function() {
if (typeof Array.prototype.random != "undefined") {
throw "Cannot redefine Array.prototype.random";
}
Array.prototype.random = random;
fn.apply(this, arguments);
if (Array.prototype.random !== random) {
throw "Array.prototype.random was redefined";
}
Array.prototype.random = undefined;
}
}
We can still use it like we used to, but we have to make sure that we execute the function that it now returns:
extendArrayWithRandom(function() {
console.log([1, 2, 3, 4, 5].random());
})();
Notice the extra () at the end, which means that we execute the function directly when it’s returned to us. Since we’re now returning a function instead, we can get back to our Ajax example:
extendArrayWithRandom(function() {
loadJson("/settings", extendArrayWithRandom(function(settings) {
var rand = settings.random();
}));
console.log([1, 2, 3, 4, 5].random());
});
Since we have two functions that will execute separatly, we need to call extendArrayWithRandom twice to make sure that these two functions will have Array.prototype.random, and that we clear it right after they have executed.
Conclusion
Adding methods to global objects is something that you should be quite careful with, simply because it changes the global state of your application, and it’s hard to predict the implications of that.
This method isn’t foolproof, since you can be affecting others by using it. As soon as you call another function in another module inside your extendArrayWithRandom, Array.prototype.random will be accessible to them as well. Now that probably won’t cause trouble, but it’s hard to know for sure.
About this entry
You’re currently reading "Extending built in objects in a safer way in Javascript", an entry on The Coffeescripter
- Published:
- 2011.05.15 at 12.55
- Comments:
- 8 Comments
- Category:
- Javascript
8 Comments
Jump to comment form | comments rss [?] | trackback uri [?]