Extending a primitive

Back

A while back I implemented a listener system, which in setTimeout / setInterval style returned a number as a reference to later be used in unregistering.

Wanting now to add a few methods to the returned reference, got me thinking about the viability of extending Javascript primitive types.

Firstly, can it be done?

Well if we try the easiest way first, by add functions directly to our variable, it fails.

var a = 10;

a.custom = () => console.log("Hello");

a.custom();

Uncaught TypeError: a.custom is not a function

at VM1259:5

In short no, primitives are by nature simple. Deliberately so infact, and in being so, are faster to initialise and process. But if we’re happy to create our primitives as objects instead… then yes. So let’s try that.

var a = new Number(10);

a.custom = () => console.log(“Hello”);

a.custom();

"Hello"

This works great, but when we want to add more and more functionality to our number, it starts to become cumbersome. So why don’t we just create our own custom object type that extends a Number?

function CustomNumber () { }

CustomNumber.prototype.__proto__ = Number.prototype;

var a = new CustomNumber(10);

a.toFixed(0);

Uncaught TypeError: Method Number.prototype.toFixed called on incompatible receiver [object Object]

at CustomNumber.toFixed (native)

at VM2046:5:5

Cool, it kind of works but none of the functions we want work? If we inspect our object and compare it against a normally new’d number we can see it’s not been properly initialised.

  • CustomNumber {}
    • proto: Number
  • Number {[[PrimitiveValue]]: 10}
    • proto: Number
    • [[PrimitiveValue]] : 10

As we can’t ensure every browsers implementation is going to be the same, we don’t want to start manually storing the number’s value using symbols (as in the example above). So why don’t we just use the normal constructor?

function CustomNumber (n) {
    Number.call(this, n);
}

CustomNumber.prototype.__proto__ = Number.prototype;

var a = new CustomNumber(10);

a.toFixed(0);

Uncaught TypeError: Method Number.prototype.toFixed called on incompatible receiver [object Object]

at CustomNumber.toFixed (native)

at VM2524:9

Nope, still no dice, so we’re going to have to get a bit more creative here. How about initialising our Number first, then substituting in our prototype?

function CustomNumber (n) { }

CustomNumber.prototype.__proto__ = Number.prototype;

function CustomNumberHelper (n) {
    n = new Number(n);
    n.__proto__ = CustomNumber.prototype;
    return n;
}

var a = CustomNumberHelper(10);

a.toFixed(0);

10

Nice, it works, but we don’t really want to be using a helper function but rather be new’ing our object the usual way. So combining them we get this.

function CustomNumber (n) {
    if (!(n instanceof Number)) {
        var n = new Number(n);
        n.__proto__ = CustomNumber.prototype;
        return n;
    }
}

CustomNumber.prototype.__proto__ = Number.prototype;

var a = new CustomNumber(10);

a.toFixed(0);

10

Awesome success, now let’s start adding our custom functions;

CustomNumber.prototype.minusFive = function () {
    return this - 5;
}

a.minusFive();

5

Now couldn’t we automate this if we wanted to extend more than just Numbers?

((_) => {
    _.extend = (original, custom) => {
        custom = custom || function () { };

        function constructor (a) {
            if (!(a instanceof original)) {
                var o = new original(...arguments);
                o.__proto__ = constructor.prototype;
                return custom.apply(o, arguments) || o;
            }
        }

        constructor.prototype = custom.prototype;
        constructor.prototype.__proto__ = original.prototype;
        return constructor;
    }
})(window._ || (window._ = {}));

var MyNumber = _.extend(Number);

MyNumber.prototype.addFive = function () {
    console.log(this + 5);
}

new MyNumber(10).addFive();

var MyString = _.extend(String, function () {
    console.log(`You made ${this}`);
});

MyString.prototype.hello = function () {
    return "Hello, " + this;
}

new MyString("Dom").hello();

15

You made Dom

"Hello, Dom"

So should you do this?

Well, no probably not. Primitives are great!

Can you do it, yes! But since you can pass any kind of object in as your original constructor, you could just use this as a basis for extending any thing. Winner winner, chicken dinner.