Extend primitives without prototyping them

I am working on a pretty ugly library that allows you to do some weird stuff. With a graph, you can map a collection of collections in a chain-like style, and when you change a value that needs to be changed throughout the system.

The problem occurred when the final type is a JS primitive.

In my case, after creating a graph with values ​​and objects, I can do something like this:

CHAIN.components[0].value = 20; 

components is a filter function over graph nodes using setters and getters. If there is only one node in the components, then the default value set by the user will be available without this: CHAIN.components.value = 20; But rather it is: CHAIN.components = 20;

Now the problem is that node can have other methods or properties besides the default value (which in my case is set to value .

How can I use setters and getters on the Number object without hacking into Number.prototype, because CHAIN.components now a number (if it's not a primitive, I made it work in an unobtrusive way), but when I want to call CHAIN.components.func() , the problem arises because I will have to add func to Number.prototype every time I create a set or get components , and then delete it.

Do you have another idea for doing this?

You need a code, here it is:

 /*jslint nomen: true, sloppy: true*/ GRID.modules.OHM || Object.extend(GRID.modules, ( function() { var Node, Nodes, Ohm, num_proto = Number.prototype.__clone(), str_proto = String.prototype.__clone(); Node = function(uid) { var UID = uid; this.getUID = function() { return UID; }; }; Nodes = function() { var stack = []; this.add = function(id, val) { var n = new Node(stack.length); val.id = id; Object.extend(n, val); stack.push(n); return n.getUID(); }; this.getById = function(id) { return stack.filter(function(v) { var a = id || v.id; return (v.id === a); }); }; this.getByUID = function(UID) { return stack[UID]; }; this.get = function(callback) { !Object.isString(callback) || ( callback = [callback]); var f = Object.isFunction(callback) ? callback : (Object.isArray(callback) ? function(k) { return (callback.indexOf(k.id) >= 0); } : function(k) { return true; }); return stack.filter(f); }; }; Ohm = function(n) { var graph = n || (new Nodes()), filters = {}, __nodes = {}, addGS = function(obj, name, conf, binder) { var alfa = {}; Object.extend(alfa, conf); if (!alfa.get) { alfa.get = function() { var a = this.g.getById(this.p); return a.length === 1 ? a[0] : a; }.bind(binder); } else { alfa.get = alfa.get.bind(binder); } if (!alfa.set) { alfa.set = function(value) { this.g.getById(this.p).forEach(function(k) { Object.extend(k, value); return true; }); }.bind(binder); } else { alfa.set = alfa.set.bind(binder); } Object.defineProperty(obj, name, alfa); }, add = function(id, node) { if (__nodes.hasOwnProperty(id)) { addGS(__nodes, id, { enumerable : true }, { t : this, p : id, g : graph }); } return graph.add(id, node || {}); }; Object.extend(this, { add : function() { add.apply(this, arguments); }, map : function(name, f, that) { var n = name, filterer = ['add', 'map', '__all']; n = Object.isFunction(n) ? name.apply(that, arguments.slice(3)) : n; if (filterer.indexOf(n.toLowerCase()) >= 0) { console.log("You can't map over a basic property of object !!! Please read the freakin' manual."); return null; } if (!filters.hasOwnProperty(n)) { filters[n] = new Ohm(graph); addGS(this, n, { get : function() { this.g.get(this.f).forEach(function(v, key, arr) { var temp, binder; if (arr.length !== 1) { if (!this.filt.hasOwnProperty(v.id)) { addGS(this.filt, v.id, { set : function(value) { this.tggetById(this.p).filter(this.tf).forEach(function(k) { Object.extend(k, value); }); }, get : function() { var a = this.tggetById(this.p).filter(this.tf); return a.length === 1 ? a[0] : a; } }, { t : this, p : v.id }); (key !== arr.length - 1) || Object.extend(this.filt, this.g.get(this.f)); } } else { if (Object.isFunction(v.__new__)) { v.__default = function() { return Object.extend((new this.__new__(arguments)), this); }; } if (!Object.isUndefined(v.__default)) { temp = this.filt; this.filt = Object.isFunction(v.__default) ? v.__default.bind(v) : v.__default; if (Object.isNumber(this.filt) || Object.isString(this.filt)) { var prot = Object.isNumber(this.filt) ? Number : String; for (var i in temp) { if (temp.hasOwnProperty(i) && !prot.prototype.hasOwnProperty(i)) { var bin = { t : temp, m : i, p : prot, }; Object.defineProperty(prot.prototype, i, { set : function(value) { Object.defineProperty(this.p.prototype, this.m, { configurable : true, // defaults to false writable : false, value : 1 }); delete this.p.prototype[this.m]; this.t[this.m] = value; }.bind(bin), get : function() { Object.defineProperty(this.p.prototype, this.m, { configurable : true, // defaults to false writable : false, value : 1 }); delete this.p.prototype[this.m]; return this.t[this.m]; }.bind(bin), enumerable : true, configurable : true }); } } } else { Object.extend(this.filt, temp); } } if (Object.isNumber(this.filt) || Object.isString(this.filt)) { var prot = Object.isNumber(this.filt) ? Number : String; for (var i in v) { if (v.hasOwnProperty(i) && !prot.prototype.hasOwnProperty(i)) { var bin = { t : v, m : i, p : prot, }; Object.defineProperty(prot.prototype, i, { set : function(value) { Object.defineProperty(this.p.prototype, this.m, { configurable : true, // defaults to false writable : false, value : 1 }); delete this.p.prototype[this.m]; this.t[this.m] = value; }.bind(bin), get : function() { Object.defineProperty(this.p.prototype, this.m, { configurable : true, // defaults to false writable : false, value : 1 }); delete this.p.prototype[this.m]; return this.t[this.m]; }.bind(bin), enumerable : true, configurable : true }); } } } else { Object.extend(this.filt, v); } } }, this); return this.filt; }, set : function(value) { this.g.get(this.f).forEach(function(k) { Object.extend(k, value); }); } }, { t : this, f : f, g : graph, filt : filters[n] }); } } }, true, true); addGS(this, '__all', { get : function() { var a = this.g.getById(); Object.extend(__nodes, a.length === 1 ? a[0] : a); return __nodes; }, enumerable : true }, { t : this, p : null, g : graph }); }; window['Ξ©'] = Ohm; return { OHM : Ohm, }; }())); 

And here is the demo:

 var c = new Ξ©(); c.add('ann', { __default : 58, blah : 98, ceva : function() { console.log('asd'); } }); c.add('ann2',{ __default: function(){ console.log('hello'); }, abc: 78, dce: function(){ console.log(' world'); } }; c.add('b2', { __new__ : function() { this.init = function() { this.id = 86; }; this.mer = function() { console.log(this); }; }, els : 'asadar' }); c.map('b2', function(k) { return k.id === 'b2'; }); c.map('ann', function(k) { return k.id === 'ann'; }); c.map('ann2', function(k) { return k.id === 'ann2'; }); console.log(c.ann); // returns 58 ( the __default value ) console.log(c.ann.blah); // returns 98 console.log(c.ann.blah.blah); // undefined console.log(c.ann2); // function() c.ann2(); // prints out 'hello' c.ann2.cde(); // prints out 'world' c.ann2 = 60; console.log(c.ann2); // 60 console.log(c.ann2.cde()); // prints out 'world' 

This code works, but the part in which I have to use the prototype Number or String bothers me. Do you have another way to do this?

The reason is to do something that someone said that it can be done in PHP but not JS, this guy recently worked with me in WebGL shaders and hated that he had to write 700 lines of code to use several effects combined with FBO instead of 100, which would take from him a similar tool like the one that was written in PHP. So, yes, I know that accessors on primitive prototypes are a hack, but how can I do it differently without using valueOf if the final object of the chain is primitive?

+4
source share
2 answers

OK, now I read the attached code (but could not execute it all). There are not enough comments, but I will not judge it, my own codes are not better, because I do not expect anyone to read them or understand :-)

You are right, the part in which you distribute your own prototypes is scary. From what I understand, you define the accessor property in the prototype Number or String before returning number / string. Both upon receipt and upon installation, the accessor property is overwritten by the data (???) property, then the entire property is deleted before you store / return the value. This seems to be a smart hack that allows you to set properties for primitive values, but:

  • There is a high risk of collisions. Perhaps this can be reduced by using the this value as the key in the lookup table (to distinguish (5).x from (3).x ), but it still cannot be completely avoided.
  • Properties that are deleted during access / configuration are extremely unintuitive. Avoid this.
  • arbitrarily changing access properties on prototype primitives are expensive. These are 4 performance combinations. No engine can optimize this. And you seem to use them quite often.

If you really need it (I still do not understand your reason), I would use the option with a lookup table. It should reduce collisions (not sure how your code handled them), does not change the access properties that they are defined, and therefore more stable (although it can occur then):

 // Let 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 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".xxx….x "loop" 

However, there is one thing you can never do with:

Unlike objects, primitive values ​​are not unique; they do not have an identity.

You can never distinguish c.ann from 58 or "loop" from "loop".x , and therefore both will have a property or not. This is not a good prerequisite for building an API.

So, I still recommend using Number and String objects. You do not need to subclass them (as shown in my previous answer), since you do not have (m) any methods on them, so you can easily create them:

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

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

but how can I make it different without using valueOf if the final object of the chain is primitive?

To answer this simple question: this guy was right, it is impossible to do this in JavaScript without breaking your own prototypes. And you're right, the hack is pretty ugly :-)

+3
source

Do not make it a primitive number, make it an object that behaves like a number. You can use the Number object and extend it with your custom properties (see fooobar.com/questions/324153 / ... ), but why not subclass Number at all? You only need to save the valueOf method, which returns a number:

 function MyNumber(n) { this.value = Number(n); } MyNumber.prototype = Object.create(Number.prototype, {constructor:{value:MyNumber}}); MyNumber.prototype.valueOf = function() { return this.value; }; // OK, we have to overwrite those methods as they don't work on subclasses of Number ["toExponential", "toFixed", "toLocaleString", "toPrecision", "toString"].forEach(function(name) { MyNumber.prototype[name] = function() { return Number.prototype[name].apply(new Number(this.value), arguments); }; }); // now extend the prototype with other properties, eg chaining methods 

Of course, you may need to make a components setter that converts the given number (or number) to MyNumber:

 var actualNumber = new MyNumber; Object.defineProperty(MyFabulousChainingThing, "components", { get: function() { return actualNumber; }, set: function(x) { actualNumber.value = Number(x); } }); 
+1
source

All Articles