Extend primitives without prototyping them

OK, now I’ve read the attached code (but could not follow all of it). Lacks some comments, but I won’t bitch about that, my own codes are not better as I don’t expect anybody to read or understand them 🙂

You are right, the part where you extend the native prototypes is scary. From what I understand, you define an accessor property on the Number or String prototype before returning a number / string. Both on getting and setting, the accessor property is overwritten by a data property (???), then the whole property is deleted, before you store / return the value. This seems to be a clever hack to allow custom properties on primitive values, yet:

  • There is a high risk of collisions. It might be possible to lower that by using the this value as a key in a lookup table (to differentiate (5).x from (3).x), but it still can’t be fully avoided.
  • Properties that remove themselves on accessing / setting are extremely unintuitive. Avoid that.
  • arbitrarily changing accessor properties on the prototype of primitives are costly. That’s 4 performance-contras combined. No engine will be able to optimize this. And you seem to use them quite often.

If you really needed this (I still didn’t get your reason), I’d use a variant with a lookup-table. It should reduce collisions (not sure how your code handled them), does not change the accessor properties ones they’re defined and is therefore more persistent (though it might leak then):

// Let's call this
// PRIMITIVE PROXIES
// as they proxy real objects behind primitive values
var proxy = _.map( { // some map that works on Objects
    string: String.prototype,
    number: Number.prototype
}, function closure(type, proto) {
    var table = {};
    function setupProperty(prop) {
        if (prop in proto) return; // ah, we already proxied this kind of object
        Object.defineProperty(proto, prop, {
            configurable:true, // for deleting
            get: function getter() {
                // "this" is the primitive value
                if (!this in table)
                    return undefined;
                return table[this][prop]; // get prop from obj
            },
            set: function setter(val) {
                if (this in table)
                    table[this][prop] = val; // pass val to obj
            }
        });
    }
    return {
        create: function createProxy(prim, obj) {
            if (prim in table) // we already did create a proxy on this primitive
                return; // let's abort. You might continue to overwrite
            table[prim] = obj;
            Object.getOwnPropertyNames(obj).forEach(setupProperty);
            return prim; // the new "proxy"
        },
        move: function moveName(from, to) {
            if (to in table) return false;
            table[to] = table[from];
            delete table[from];
            return true;
        }
    };
});
proxy.create = function(prim, obj) {
    return proxy[typeof prim].create(prim, obj);
};
proxy.move = function(from, to) {
    return proxy[typeof from].create(from, to);
};
// USAGE:
// proxy.create works just like Object.extend
> var c = {ann: 58},
>     o = {blah: 98};
> proxy.create(c.ann, o);
> 58..blah
98
> c.ann.blah
98
> (58).blah = 60;
> o
{blah: 60}
> var num = c.ann; // 58
> c.ann.blah = function(){return "Hello"};
> num.blah()
"Hello"
> proxy.move(c.ann, c.ann = 78);
> c.ann
78
> (58).blah
undefined
> c.ann.blah()
"Hello"
> // getters/setters for properties are global:
> c.ann.blub = "something"; // does not work, there is no getter
> c.ann.blub
undefined
> proxy.create(58, {blub: "foo"})
> c.ann.blub // still returns
undefined
> c.ann.blub = "bar"; // but can be set now
> (58).blub + (78).blub
"foobar"
> // infinite lookup loops are possible:
> proxy.create("loop", {x:"loop"});
> "loop" === "loop".x
true
> "loop".x.x.x.….x
"loop"

However, there is one thing you will never be able to work around:

Unlike objects, primitive values are not unique; they have no identity.

You will never be able to distinguish c.ann from 58, or "loop" from "loop".x, and so both will have a property or not. This is not a good premise to build an API upon.

So, I still recommend to use Number and String objects. You don’t need to subclass them (as shown in my previous answer), as you don’t seem to have (m)any methods on them, so you can easily build them:

c.ann = new Number(58);
c.ann.blah = 98;
return c;

There should be hardly a difference, expect for the typeof operator. Could you maybe add some more examples that use the __default value?

but how can I make it different without having to use valueOf if the chain end object is a primitive?

To answer this simple question: That guy was right, it cannot be done in JavaScript without hacking native prototypes. And you are right, the hack is quite ugly 🙂

Leave a Comment