Deploy javascript instance storage by returning an existing instance from the constructor

I am trying to implement my version of the “store instance” in Backbone.js, as Soundcloud described in their recent blog post:

http://backstage.soundcloud.com/2012/06/building-the-next-soundcloud/

Appropriate exposure:

To solve this problem, we use a construct that we call instance storage. This store is an object that is implicitly accessible and modified every time the constructor for the model is called. When the model was first built, it is embedded in the repository using its identifier as a unique key. If the same model constructor is called with the same identifier, the original instance is returned.

var s1 = new Sound({id: 123}), s2 = new Sound({id: 123}); s1 === s2; // true, these are the exact same object. 

This works because of the surprisingly little-known Javascript feature. If the constructor returns an object, then this is the assigned value. Therefore, if we return the link to the instance created earlier, we will get the desired behavior. Behind the scenes, the constructor basically does this:

 var store = {}; function Sound(attributes) { var id = attributes.id; // check if this model has already been created if (store[id]) { // if yes, return that return store[id]; } // otherwise, store this instance store[id] = this; } 

I implemented my version of this by overriding the Backbone.Model class to create my own constructor.

 var MyModel = Backbone.Model.extend({ constructor: function (attributes, options) { var id = attributes ? attributes.id : undefined; if (this.store[id]) { return this.store[id]; } Backbone.Model.prototype.constructor.apply(this, arguments); if (id) { this.store[id] = this; } } }); var MyOtherModel = MyModel.extend({ store: {}, //other model stuff }); 

This worked fine, but something had to change, and now it stopped working, and I'm not sure why. Recently created instances are stored in the storage object without any problems - each class that extends the MyModel class has its own empty storage to avoid collisions of instances of another type with the same identifier. The correct instance is also retrieved without problems when calling the constructor with an existing identifier, however, when they return from the constructor, the return value is ignored. My understanding from the specification is that constructors can return an object, but not a primitive, and the returned object will be assigned to the left side of the assignment operator when the constructor is called with a new operator. This does not happen, even if the constructor returns an object, an empty object created by the new operator is used.

Some debugging information. Not sure how useful this information will be. This is "this" in the MyModel constructor for the object that is being created for the first time.

 child _callbacks: Object _escapedAttributes: Object _previousAttributes: Object _setting: false attributes: Object id: "4fd6140032a6e522f10009ac" manufacturer_id: "4f4135ae32a6e52a53000001" name: "Tide" uniqueName: "tide" __proto__: Object cid: "c50" collection: child id: "4fd6140032a6e522f10009ac" __proto__: ctor constructor: function (){ parent.apply(this, arguments); } defaults: Object store: Object url: function () { urlRoot: function () { __proto__: ctor 

And this is the "this" in the MyModel constructor when the object is returned from the instance store:

 child _callbacks: Object _escapedAttributes: Object _previousAttributes: Object _setting: false attributes: Object _validate: function (attrs, options) { bind: function (events, callback, context) { change: function (options) { changedAttributes: function (diff) { clear: function (options) { clone: function () { constructor: function (){ parent.apply(this, arguments); } defaults: Object destroy: function (options) { escape: function (attr) { fetch: function (options) { get: function (attr) { has: function (attr) { hasChanged: function (attr) { idAttribute: "id" initialize: function (){} isNew: function () { isValid: function () { manufacturer_id: 0 name: "" off: function (events, callback, context) { on: function (events, callback, context) { parse: function (resp, xhr) { previous: function (attr) { previousAttributes: function () { save: function (key, value, options) { set: function (key, value, options) { store: Object toJSON: function () { trigger: function (events) { unbind: function (events, callback, context) { unset: function (attr, options) { url: function () { urlRoot: function () { __proto__: Object cid: "c141" __proto__: ctor constructor: function (){ parent.apply(this, arguments); } defaults: Object store: Object url: function () { urlRoot: function () { __proto__: ctor 

I note that the attribute object in the second has all the methods of the base object included in it, which they should not be. It also has no identifier, again I'm not sure why. Hope this gives some insight. Thanks.

+6
source share
3 answers

I would not use the extension for this, I think a separate "factory" is the right idea. This will allow you to expand your models without fear of side effects.

From an annotated source, the skeleton does some strange things with the extension, I did not completely wrap my head around me. (Also check the inherits ) So let's skip it now and stick to your working solution.

I changed your method to create factory models, you should use them as regular models (for example, install them in collections), except that they will not work. They will also process updates to your models with new data, such as the soundcloud example.

 var makeStoreable = function(model){ var StoreModel = function(attr, opt){ if(!attr || !attr.id){ // The behavior you exhibit here is up to you throw new Error('Cool Models always have IDs!'); } if(this.store[attr.id]){ this.store[attr.id].set(attr, opt); }else{ var newModel = new model(attr, opt); this.store[attr.id] = newModel; } return this.store[attr.id]; }; StoreModel.prototype.store = {}; return StoreModel; }; var CoolModel = Backbone.Model.extend({}); CoolModel = makeStoreable(CoolModel); var a = new CoolModel({ id: 4, coolFactor: 'LOW' }); var b = new CoolModel({ id:4, coolFactor: 'HIGH' }); console.log(a===b); //true! console.log(a.get('coolFactor') === 'HIGH'); //true! 

And here is a fiddle to play with.

In addition, I would welcome someone to come up with a model solution containing a “repository” in the prototype model instances. In addition, to prevent memory leaks, we should probably create a reference counting method for both the factory and the model itself.

+3
source
An approach

@wizard seems pretty nice and clean. +1 to this.

The way it is implemented in SoundCloud is to override the Backbone.Model.extend method to create a class with our modified constructor and repository in closure. At first, the repository was created in closing to keep the interface class clean, but after a while it was considered useful for debugging to have a link to each class repository, so it was also attached there.

We have a reference counter, so memory usage does not explode, and also gives classes the ability to define a user-defined function that provides a unique value for identifying it. In most cases, id sufficient, but there are some angular cases where it does not quite work.

I would welcome someone to come up with a solution to the model, keeping the "store" in the prototype of the model instances

You can do myInstance.constructor.store

+2
source

After using the @reconbot solution, I found that it calls the instanceof operator:

 (new CoolModel) instanceof CoolModel // FALSE!!! 

and

 var MyModel = Backbone.Model.extend({ idAttribute: 'myId' }); new MyModel({ myId: 1 }) === new MyModel({ myId: 1 }) // FALSE! 

I developed a new version that uses the property of the model’s own identifier (via idAttribute) and works with instanceof and allows you to expand the factory:

Fiddle

 function makeStoreable(model) { var store = {}; var idField = model.prototype.idAttribute; function ModelFactory(attr, opt) { if (!attr || !(idField in attr)) { throw new Error('Cool Models always have IDs!'); } var id = attr[idField]; if (store.hasOwnProperty(id)) { store[id].set(attr, opt); } else { model.call(this, attr, opt); store[id] = this; } return store[id]; } function intermediate() {} intermediate.prototype = model.prototype; ModelFactory.prototype = new intermediate; // Only EcmaScript5! // ModelFactory.extend = model.extend.bind(model); ModelFactory.extend = function() { return model.extend.apply(model, arguments); }; return ModelFactory; } 

And the test:

 var RareID = Backbone.Model.extend({ idAttribute: '_myOwnServerId' }); RareID = makeStoreable(RareID); var a = new RareID({ _myOwnServerId: 4, coolFactor: 'LOW' }); var b = new RareID({ _myOwnServerId: 4, coolFactor: 'HIGH' }); console.log(a===b); //true! console.log(a instanceof RareID); //true! console.log(a.get('coolFactor') === 'HIGH'); //true! 

:)

0
source

Source: https://habr.com/ru/post/918692/


All Articles