I understand this is a problem (or behaviour) of javascript itself rather than Backbone's extend method, but I'd like to know what is the best strategy to avoid it.
Let's better put it in code:
var MyModel = Backbone.Model.extend({
value: 0,
values: []
});
var myFirstModel = new MyModel();
myFirstModel.value // 0, as expected
myFirstModel.values // [], as expected
var mySecondModel = new MyModel();
mySecondModel.value = 2;
mySecondModel.values.push(2)
mySecondModel.value // 2, as expected
mySecondModel.values // [2], as expected
myFirstModel.value // 0, as expected
myFirstModel.values // [2], ... WAT!!!
I do understand that the problem is I'm not assigning a new value to mySecondModel.values I'm just operating on the values variable that is in the prototype, that is MyModel.prototype.values (same problem with any other object, of course)
But it's very easy to mess with that. The most intuitive thing is to just think of those as INSTANCE variables, and not variables common to every instance (static or class variables in class based languages).
So far now the general solution I've found is to initialize every variable in the initialize method, like this:
var MyModel = Backbone.Model.extend({
initialize: function() {
this.value = 0;
this.values = [];
}
});
That way everything works as expected, and even though it wouldn't be neccesary for a simple value (like this.value) I find it much easier to just stick to this prnciple in every case.
I'm wondering if there's some better (more elegant, clearer) solution to this problem
This is an effect of JavaScript's prototypical inheritance and the fact that Array objects are reference types. The key/value pairs of the object you pass to extend are copied onto the prototype of MyModel, so they will be shared by all instances of MyModel. Because values is an array, when you modify it, you modify the array for every instance.
What you are doing by setting values inside initialize is called shadowing the prototype, and it is the correct way to solve this issue.
That said, in the case of Backbone.Model, if you are attempting to deal with the model's attributes, you can use the defaults function to provide defaults like this:
var MyModel = Backbone.Model.extend({
defaults: function() {
return {
value: 0,
values: []
}
}
});
Again, this is only for attributes of an instance.
var inst = new MyModel();
// The defaults will be created for each new model,
// so this will always return a new array.
var values = inst.get('values');
For what you are doing, where you are specifying properties on the model itself, it is up to you to set the defaults inside of initialize, as you have done.
Are you intentionally not setting value and values as backbone attributes? If you set attributes on an instance, instead of putting them in the extended backbone model definition, it might work how you expect.
var MyModel = Backbone.Model.extend();
var myFirstModel = new MyModel({
value: 0,
values: []
});
console.log(myFirstModel.get('value'); // 0
console.log(myFirstModel.get('values'); // []
var mySecondModel = new MyModel({
value: 2,
values: [2]
});
//mySecondModel.value = 2;
//mySecondModel.values.push(2)
console.log(mySecondModel.get('value'); // 2
console.log(mySecondModel.get('values'); // [2]
console.log(myFirstModel.get('value'); // 0
console.log(myFirstModel.get('values'); // []
jsFiddle, check the console log.
I too had stumbled across this problem some time back and solved it by defining a defaults method in the model.
var MyModel = Backbone.Model.extend({
defaults: function() {
return {
value: 0,
values: []
}
}
});
Related
I am new to "object-oriented" JavaScript. Currently, I have an object that I need to pass across pages. My object is defined as follows:
function MyObject() { this.init(); }
MyObject.prototype = {
property1: "",
property2: "",
init: function () {
this.property1 = "First";
this.property2 = "Second";
},
test: function() {
alert("Executing test!");
}
}
On Page 1 of my application, I am creating an instance of MyObject. I am then serializing the object and storing it in local storage. I am doing this as shown here:
var mo = new MyObject();
mo.test(); // This works
window.localStorage.setItem("myObject", JSON.stringify(mo));
Now, on Page 2, I need get that object and work with it. To retrieve it, I am using the following:
var mo = window.localStorage.getItem("myObject");
mo = JSON.parse(mo);
alert(mo.property1); // This shows "First" as expected.
mo.test(); // This does not work. In fact, I get a "TypeError" that says "undefined method" in the consol window.
Based on the outputs, it looks like when I serialized the object, somehow the functions get dropped. I can still see the properties. But I can't interact with any of my functions. What am I doing wrong?
JSON doesn't serialize functions.
Take a look at the second paragraph here.
If you need to preserve such values, you can transform values as they are serialized, or prior to deserialization, to enable JSON to represent additional data types.
In other words, if you really want to JSONify the functions, you can convert them to strings before serializing:
mo.init = ''+mo.init;
mo.test = ''+mo.test;
And after deserializing, convert them back to functions.
mo.init = eval(mo.init);
mo.test = eval(mo.test);
However, there should be no reason to do that. Instead, you can have your MyObject constructor accept a simple object (as would result from parsing the JSON string) and copy the object's properties to itself.
Functions can not be serialized into a JSON object.
So I suggest you create a separate object (or property within the object) for the actual properties and just serialize this part.
Afterwards you can instantiate your object with all its functions and reapply all properties to regain access to your working object.
Following your example, this may look like this:
function MyObject() { this.init(); }
MyObject.prototype = {
data: {
property1: "",
property2: ""
},
init: function () {
this.property1 = "First";
this.property2 = "Second";
},
test: function() {
alert("Executing test!");
},
save: function( id ) {
window.localStorage.setItem( id, JSON.stringify(this.data));
},
load: function( id ) {
this.data = JSON.parse( window.getItem( id ) );
}
}
To avoid changing the structure, I prefer to use Object.assign method on object retrieval. This method merge second parameter object in the first one. To get object methods, we just need an empty new object which is used as the target parameter.
var mo = window.localStorage.getItem("myObject");
// this object has properties only
mo = JSON.parse(mo);
// this object will have properties and functions
var completeObject = Object.assign(new MyObject(), mo);
Note that the first parameter of Object.assign is modified AND returned by the function.
it looks like when I serialized the object, somehow the functions get dropped... What am I doing wrong?
Yes, functions will get dropped when using JSON.stringify() and JSON.parse(), and there is nothing wrong in your code.
To retain functions during serialization and deserialization, I've made an npm module named esserializer to solve this problem -- the JavaScript class instance values would be saved during serialization on Page 1, in plain JSON format, together with its class name information:
var ESSerializer = require('esserializer');
function MyObject() { this.init(); }
MyObject.prototype = {
property1: "",
property2: "",
init: function () {
this.property1 = "First";
this.property2 = "Second";
},
test: function() {
alert("Executing test!");
}
}
MyObject.prototype.constructor=MyObject; // This line of code is necessary, as the prototype of MyObject has been overridden above.
var mo = new MyObject();
mo.test(); // This works
window.localStorage.setItem("myObject", ESSerializer.serialize(mo));
Later on, during the deserialization stage on Page 2, esserializer can recursively deserialize object instance, with all types/functions information retained:
var mo = window.localStorage.getItem("myObject");
mo = ESSerializer.deserialize(mo, [MyObject]);
alert(mo.property1); // This shows "First" as expected.
mo.test(); // This works too.
That's because JSON.stringify() doesn't serialize functions i think.
You're right, functions get dropped. This page might help:
http://www.json.org/js.html
"Values that do not have a representation in JSON (such as functions and undefined) are excluded."
This question already has an answer here:
Backbone View extends is polluted
(1 answer)
Closed 8 years ago.
I spent a lot of time trying to catch a bug in my app. Eventually I set apart this piece of code which behavior seems very strange to me.
var Model = Backbone.Model.extend({
myProperty: []
});
var one = new Model();
var two = new Model();
one.myProperty.push(1);
console.log(two.myProperty); //1!!
What's the reason behind it? Why it acts so? How to avoid this type of bugs in code?
Inheritance in JavaScript is prototypical - objects can refer directly to properties higher up in the prototype chain.
In your example, one and two both share a common prototype, and do not provide their own values for myProperty so they both refer directly to Model.protoype.myProperty.
You should create new myProperty array for each model you instantiate. Model.initialize is the idiomatic place for this kind of initialisation - overriding constructor is unnecessarily complex.
var Model = Backbone.Model.extend({
initialize: function() {
this.myProperty = [];
}
});
Alternatively you could make myProperty as an attribute of the model:
var Model = Backbone.Model.extend({
defaults: function() {
return {
myProperty: []
}
}
});
It is important to note that defaults is a function - if you were to use a simple object you would encounter the same shared reference issue.
Actually its because myProperty is an array, and as you know arrays will be stored by reference. Just to test consider the following code:
var Model = Backbone.Model.extend({
myProperty: [],
messege: ''
});
var one = new Model();
var two = new Model();
one.messege = 'One!';
two.messege = 'Two!';
console.log(one.messege ); // 'One!'
console.log(two.messege ); // 'Two!'
An alternative around this could be:
var Model = Backbone.Model.extend({
constructor: function() {
this.myProperty = [];
Backbone.Model.apply(this);
}
});
var one = new Model();
one.myProperty.push(1);
var two = new Model();
console.log(two.myProperty); // []
The documentation says:
constructor / initialize new Model([attributes], [options])
When creating an instance of a model, you can pass in the initial values of the attributes, which will be set on the model. If you define an initialize function, it will be invoked when the model is created.
In rare cases, if you're looking to get fancy, you may want to override constructor, which allows you to replace the actual constructor function for your model.
So, following the documentation, you'd want to do something like this to get your case running:
var Model = Backbone.Model.extend({
initialize: function() {
this.myProperty = [];
}
});
source: http://backbonejs.org/#Model-extend
Whats the best way to access testval and testoption inside the foreach loop? This is a mootools draft.
var some = new Class({
options: { testarray: [1,2,3], testoption: 6 },
initialize: function(options) {
this.testval = '123';
this.options.testarray.each(function(el) {
console.log(this.testval);
console.log(this.options.testoption);
});
}
});
UPDATE:
I can fix it by adding bind(this) on the array, but is that the way to go?
In cases where I need to reference a number of instance variables from a function that makes this refer to something else I often use var self = this; just before. I find it reads a lot better than binding things all over the place; the self becomes explicitly clear to refer to the instance.
yes, the mootools way to do this is to bind your functions either with
this.options.testarray.each(function(el) {
console.log(this.testval);
console.log(this.options.testoption);
}.bind(this));
or by using the Binds mutator (available in Mootools More, thanks #Dimitar Christoff)
var some = new Class({
options: { testarray: [1,2,3], testoption: 6 },
Implements: Optons,
Binds: ['logOption'],
initialize: function(options) {
this.testval = '123';
this.setOptions(options);
this.options.testarray.each(this.logOptions);
},
logOptions : function(value, index, array) {
// I don't really see the point, but here you are, this code will be executed
// three times, with (1, 0, [1,2,3]), (2, 1, [1,2,3]) and (3, 2, [1,2,3])
console.log(value, index, array);
console.log(this.testval);
console.log(this.options.testoption);
}
});
I moved your each (and not forEach, as said in the comments) inside the initialize(), since I'm not sure code inside the class descriptor object mill work... Also you might want to use the passed options in initialize with this.setOptions(options) and implementing the Options mutator.
Also, as noted in every comment you have var self = this; which is very convenient AND readable.
I am using jQuery and I am still pretty new to JavaScript. I am implementing an object as the following:
MyObject = {
properties : [{}],
resetProperties: function resetProperties() { this.properties = [{}] }
};
As you can see in the above code I can reset the properties by running MyObject.resetProperties() but, in order to do that, I state two times the [{}] variable. How should I accomplish the same thing without repeating that code?
Update
I tried to do the following:
MyObject = {
properties : this.propertiesDefault,
resetProperties : function resetProperties() { this.properties = [{}] },
propertiesDefault: [{}]
};
but I get "TypeError: invalid 'in' operand MyObject.properties" and I am not sure that is the right way to proceed.
It seems to me that it would be impossible to avoid having your default / reset properties as a separate object to the one that will be modified.
I would recommend having a default value, and cloning it in your initialisation and reset function. Since you tagged your question with jQuery, I assume you are happy to clone the object with that:
MyObject = {
defaultProperties : [{}],
properties : jQuery.extend(true, {}, this.defaultProperties),
resetProperties: function() {
this.properties = jQuery.extend(true, {}, this.defaultProperties);
}
};
See this Stack Overflow question for more information on cloning objects:
What is the most efficient way to deep clone an object in JavaScript?
This is the documentation for jQuery.extend:
http://docs.jquery.com/Utilities/jQuery.extend
From what I know this isn't possible. You're going to have to hard-code the property reset. I tried setting a variable cache outside the object, but when I reset the property it unfortunately maintains its value.
var obj = {
p: [ {} ],
r: function() { this.p = this.cache; }
};
obj.cache = obj.p; // attempt to set to original
obj.p[0].m = 5; // modify
obj.r(); // reset
--------
>>> obj.p[0].m; // 5
We can assume the the cache property is being modified in the same way as p is. Therefore, we can't reset like that.
Depends on what you want. Since you're new to javascript, you may be unfamiliar with using functions to create custom objects, which is the general javascript "OOP" kinda way to do it.
function MyObjectClass() {
this.properties = null;
this.resetProperties();
}
MyObjectClass.prototype.resetProperties = function () { this.properties = [{}] };
var MyObject= new MyObjectClass();
But we don't really know that function MyObject needs to fulfill. There may be a requirement that it NEEDs to be a plain old javascript object. Or maybe not, and you're done.
Of course, you can always directly:
MyObject = {
properties : null,
resetProperties: function () { this.properties = [{}];}
};
MyObject.resetProperties();
I have a simple question about Backbone.js' get and set functions.
1) With the code below, how can I 'get' or 'set' obj1.myAttribute1 directly?
Another question:
2) In the Model, aside from the defaults object, where can/should I declare my model's other attributes, such that they can be accessed via Backbone's get and set methods?
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
}
})
var MyView = Backbone.View.extend({
myFunc: function(){
console.log(this.model.get("obj1"));
//returns the obj1 object
//but how do I get obj1.myAttribute1 directly so that it returns false?
}
});
I know I can do:
this.model.get("obj1").myAttribute1;
but is that good practice?
While this.model.get("obj1").myAttribute1 is fine, it's a bit problematic because then you might be tempted to do the same type of thing for set, i.e.
this.model.get("obj1").myAttribute1 = true;
But if you do this, you won't get the benefits of Backbone models for myAttribute1, like change events or validation.
A better solution would be to never nest POJSOs ("plain old JavaScript objects") in your models, and instead nest custom model classes. So it would look something like this:
var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});
var MyModel = Backbone.Model.extend({
initialize: function () {
this.set("obj1", new Obj());
}
});
Then the accessing code would be
var x = this.model.get("obj1").get("myAttribute1");
but more importantly the setting code would be
this.model.get("obj1").set({ myAttribute1: true });
which will fire appropriate change events and the like. Working example here: http://jsfiddle.net/g3U7j/
I created backbone-deep-model for this - just extend Backbone.DeepModel instead of Backbone.Model and you can then use paths to get/set nested model attributes. It maintains change events too.
model.bind('change:user.name.first', function(){...});
model.set({'user.name.first': 'Eric'});
model.get('user.name.first'); //Eric
Domenic's solution will work however each new MyModel will point to the same instance of Obj.
To avoid this, MyModel should look like:
var MyModel = Backbone.Model.extend({
initialize: function() {
myDefaults = {
obj1: new Obj()
}
this.set(myDefaults);
}
});
See c3rin's answer # https://stackoverflow.com/a/6364480/1072653 for a full explanation.
I use this approach.
If you have a Backbone model like this:
var nestedAttrModel = new Backbone.Model({
a: {b: 1, c: 2}
});
You can set the attribute "a.b" with:
var _a = _.omit(nestedAttrModel.get('a')); // from underscore.js
_a.b = 3;
nestedAttrModel.set('a', _a);
Now your model will have attributes like:
{a: {b: 3, c: 2}}
with the "change" event fired.
There is one solution nobody thought of yet which is lots to use. You indeed can't set nested attributes directly, unless you use a third party library which you probably don't want. However what you can do is make a clone of the original dictionary, set the nested property there and than set that whole dictionary. Piece of cake.
//How model.obj1 looks like
obj1: {
myAttribute1: false,
myAttribute2: true,
anotherNestedDict: {
myAttribute3: false
}
}
//Make a clone of it
var cloneOfObject1 = JSON.parse(JSON.stringify(this.model.get('obj1')));
//Let's day we want to change myAttribute1 to false and myAttribute3 to true
cloneOfObject1.myAttribute2 = false;
cloneOfObject1.anotherNestedDict.myAttribute3 = true;
//And now we set the whole dictionary
this.model.set('obj1', cloneOfObject1);
//Job done, happy birthday
I had the same problem #pagewil and #Benno had with #Domenic's solution. My answer was to instead write a simple sub-class of Backbone.Model that fixes the problem.
// Special model implementation that allows you to easily nest Backbone models as properties.
Backbone.NestedModel = Backbone.Model.extend({
// Define Backbone models that are present in properties
// Expected Format:
// [{key: 'courses', model: Course}]
models: [],
set: function(key, value, options) {
var attrs, attr, val;
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
_.each(this.models, function(item){
if (_.isObject(attrs[item.key])) {
attrs[item.key] = new item.model(attrs[item.key]);
}
},this);
return Backbone.Model.prototype.set.call(this, attrs, options);
}
});
var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});
var MyModel = Backbone.NestedModel.extend({
defaults: {
obj1: new Obj()
},
models: [{key: 'obj1', model: Obj}]
});
What NestedModel does for you is allow these to work (which is what happens when myModel gets set via JSON data):
var myModel = new MyModel();
myModel.set({ obj1: { myAttribute1: 'abc', myAttribute2: 'xyz' } });
myModel.set('obj1', { myAttribute1: 123, myAttribute2: 456 });
It would be easy to generate the models list automatically in initialize, but this solution was good enough for me.
Solution proposed by Domenic has some drawbacks. Say you want to listen to 'change' event. In that case 'initialize' method will not be fired and your custom value for attribute will be replaced with json object from server. In my project I faced with this problem. My solution to override 'set' method of Model:
set: function(key, val, options) {
if (typeof key === 'object') {
var attrs = key;
attrs.content = new module.BaseItem(attrs.content || {});
attrs.children = new module.MenuItems(attrs.children || []);
}
return Backbone.Model.prototype.set.call(this, key, val, options);
},
While in some cases using Backbone models instead of nested Object attributes makes sense as Domenic mentioned, in simpler cases you could create a setter function in the model:
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
},
setObj1Attribute: function(name, value) {
var obj1 = this.get('obj1');
obj1[name] = value;
this.set('obj1', obj1);
}
})
If you interact with backend, which requires object with nesting structure.
But with backbone more easy to work with linear structure.
backbone.linear can help you.