Расширять примитивы без их прототипирования

Я работаю над довольно уродливой библиотекой, которая позволяет вам делать какие-то странные вещи. Имея граф, вы можете сопоставить набор коллекций в стиле, подобном цепочке, и когда вы изменяете значение, которое должно быть изменено во всей системе.

Проблема возникла, когда конечный тип – примитив JS.

В моем случае после создания графика со значениями и объектами я могу сделать что-то вроде этого:

CHAIN.components[0].value = 20; 

components – это функция фильтра по узлам графика с использованием сеттеров и геттеров. Если в компонентах только один узел фильтруется, значение по умолчанию, заданное пользователем, будет доступно без этого: CHAIN.components.value = 20; Но скорее это: CHAIN.components = 20;

Теперь проблема в том, что узел мог иметь другие методы или свойства, кроме значения по умолчанию (которое в моем случае установлено на value .

Как я могу использовать сеттеры и геттеры на объекте Number без взлома в Number.prototype, потому что CHAIN.components теперь является номером (если это не примитив, я сделал его работу ненавязчивым способом), но когда я хочу вызовите CHAIN.components.func() возникает проблема, потому что мне придется добавлять к Number.prototype func каждый раз, когда я создаю набор или получаю components а затем удаляю его.

Есть ли у вас другая идея для осуществления такого поведения?

Вам нужен код, вот он:

 /*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, }; }())); 

И вот демо:

 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' 

Этот код работает, но часть, где я должен использовать прототип Number или String, беспокоит меня. У вас есть другой способ сделать это?

Причина в том, что кто-то сказал, что это можно сделать на PHP, но не JS, этот парень недавно работал со мной в шейдерах WebGL и ненавидел, что ему пришлось написать 700 строк кода, чтобы использовать несколько эффектов в сочетании с FBO вместо 100, который взял бы у него аналогичный инструмент, подобный этому, который был написан на PHP. Итак, да, я знаю, что аксессоры на примитивных прототипах – это взлом, но как я могу сделать его другим, не используя valueOf, если конечный объект цепи является примитивным?

Хорошо, теперь я прочитал прилагаемый код (но не мог следовать за всем этим). Не хватает комментариев, но я не буду судить об этом, мои собственные коды не лучше, так как я не ожидаю, что кто-нибудь их прочитает или поймет 🙂

Вы правы, часть, где вы продлеваете родные прототипы, пугает. Из того, что я понимаю, вы определяете свойство accessor в прототипе Number или String перед возвратом number / string. Как при получении, так и при установке свойство accessor перезаписывается свойством data (???), тогда все свойство удаляется, прежде чем вы храните / возвращаете значение. Это, кажется, умный взлом, позволяющий настраивать свойства для примитивных значений, но:

  • Существует высокий риск столкновений. Возможно, это можно снизить, используя this значение в качестве ключа в таблице поиска (чтобы отличить (5).x от (3).x ), но этого все еще нельзя полностью избежать.
  • Свойства, которые удаляются при доступе / настройке, крайне неинтуитивны. Избегайте этого.
  • произвольно изменяющиеся свойства доступа к прототипу примитивов являются дорогостоящими. Это 4 сочетания производительности. Ни один двигатель не сможет оптимизировать это. И вы, кажется, используете их довольно часто.

Если вам это действительно нужно (я все еще не понял вашу причину), я бы использовал вариант с таблицей поиска. Он должен уменьшить коллизии (не уверен, как ваш код обрабатывал их), не изменяет свойства доступа, которые они определены, и поэтому более устойчив (хотя он может протекать тогда):

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

Однако есть одна вещь, с которой вы никогда не сможете работать:

В отличие от объектов, примитивные значения не уникальны; у них нет личности.

Вы никогда не сможете отличить c.ann от 58 или "loop" от "loop".x , и поэтому оба будут иметь свойство или нет. Это не является хорошей предпосылкой для построения API.

Поэтому я по-прежнему рекомендую использовать объекты Number и String . Вам не нужно подclassифицировать их (как показано в моем предыдущем ответе), поскольку у вас нет (m) любых методов на них, поэтому вы можете легко их создать:

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

Не должно быть разницы, ожидайте для оператора typeof . Не могли бы вы добавить еще несколько примеров, которые используют значение __default ?

но как я могу сделать это иначе, не используя valueOf, если конечный объект цепочки является примитивным?

Чтобы ответить на этот простой вопрос: этот парень был прав, это невозможно сделать в JavaScript без взлома собственных прототипов. И ты прав, хак довольно уродлив 🙂

Не делайте его примитивным числом, делайте его объектом, который ведет себя как число. Вы можете использовать объект Number и расширять его с вашими настраиваемыми свойствами (см. https://stackoverflow.com/a/9338486/1048572 ), но почему бы не подclass Number вообще? Он должен только сохранить метод valueOf который возвращает число:

 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 

Конечно, вам может потребоваться сделать components сеттером, который преобразует заданное число (или число) в MyNumber:

 var actualNumber = new MyNumber; Object.defineProperty(MyFabulousChainingThing, "components", { get: function() { return actualNumber; }, set: function(x) { actualNumber.value = Number(x); } }); 
  • Выбор значения null: в чем причина «selectAll (null)» в D3.js?
  • попросите разрешение на геолокацию снова, если оно было отклонено
  • Переменные Javascript в атрибутах HTML
  • Преобразование объекта JS в массив с помощью jQuery
  • Как проверить, открыт ли порт в сети клиента / брандмауэре?
  • Каков наиболее эффективный способ конкатенации N массивов?
  • Клавиши захвата, введенные на виртуальную клавиатуру Android, используя javascript
  • Миграция AngularJS в Angular 4,5 (с DEMO)
  • setState не обновляет состояние сразу
  • Как легко поднять при запуске файла .jar?
  • Как разобрать 20-значный номер с помощью JavaScript и jQuery
  • Давайте будем гением компьютера.